wip(standalone): paused by the owner -- hashed maps + inline allocator (no speedup yet), streaming in progress
Hashed TMap/TSet and real TInlineAllocator storage did not move level 0 (72.1 -> 74.1 s, within noise): the slowness is elsewhere. StreamingWorld.h started (first build hit an int64/uint64 mismatch), library plan drafted (SHARED_CPP_LIBRARY_PLAN.md), before-normal captures saved. Uncompiled/unmeasured parts: streaming. Resume by forking session 01a0fd2e-3c3b-7050-b4e9-628fdcc4d553. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
05f62cff36
commit
3f300b912a
8 files changed
+997
-100
No files matched your search
@@ -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"
|
||||
+296
-19
@@ -19,7 +19,9 @@
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
@@ -88,13 +90,192 @@ template<class Signature> using TFunctionRef = std::function<Signature>;
|
||||
template<class T> constexpr T&& Forward(std::remove_reference_t<T>& Value){return std::forward<T>(Value);}
|
||||
template<class T> constexpr T&& Forward(std::remove_reference_t<T>&& Value){return std::forward<T>(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 T>
|
||||
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<class U> FCompatAllocator(const FCompatAllocator<U>&) noexcept {}
|
||||
|
||||
[[nodiscard]] T* allocate(std::size_t Count)
|
||||
{
|
||||
if(Count>std::numeric_limits<std::size_t>::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<std::byte*>(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<FCompatAllocationHeader*>(static_cast<std::byte*>(Data)-sizeof(FCompatAllocationHeader));
|
||||
::new (Header) FCompatAllocationHeader{Pool,Base,Total,Alignment};
|
||||
return static_cast<T*>(Data);
|
||||
}
|
||||
|
||||
void deallocate(T* Data,std::size_t) noexcept
|
||||
{
|
||||
if(!Data)return;
|
||||
auto* Header=reinterpret_cast<FCompatAllocationHeader*>(reinterpret_cast<std::byte*>(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<class U> bool operator==(const FCompatAllocator<U>&)const noexcept{return true;}
|
||||
template<class U> bool operator!=(const FCompatAllocator<U>&)const noexcept{return false;}
|
||||
};
|
||||
|
||||
template<class Allocator> struct FCompatInlineCapacity { static constexpr std::size_t Value=0; };
|
||||
template<int32 N> struct FCompatInlineCapacity<TInlineAllocator<N>>
|
||||
{ static constexpr std::size_t Value=static_cast<std::size_t>(N); };
|
||||
|
||||
template<class T,std::size_t InlineCount,class Allocator=FCompatAllocator<T>>
|
||||
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<T,Allocator> NewHeap;NewHeap.reserve(Count);
|
||||
for(std::size_t I=0;I<InlineSize;++I)NewHeap.emplace_back(std::move(InlineData()[I]));
|
||||
DestroyInline();Heap=std::move(NewHeap);InlineSize=0;bHeap=true;
|
||||
}
|
||||
|
||||
void clear()noexcept
|
||||
{if(bHeap)Heap.clear();else DestroyInline();}
|
||||
void resize(std::size_t Count)
|
||||
{
|
||||
const std::size_t Old=size();
|
||||
if(Count<Old)
|
||||
{
|
||||
if(bHeap)Heap.resize(Count);
|
||||
else{while(InlineSize>Count)InlineData()[--InlineSize].~T();}
|
||||
return;
|
||||
}
|
||||
reserve(Count);
|
||||
while(size()<Count)emplace_back();
|
||||
}
|
||||
void assign(std::size_t Count,const T& Value)
|
||||
{clear();reserve(Count);for(std::size_t I=0;I<Count;++I)emplace_back(Value);}
|
||||
template<class It> 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<class... Args> reference emplace_back(Args&&... ArgsIn)
|
||||
{
|
||||
if(bHeap){Heap.emplace_back(std::forward<Args>(ArgsIn)...);return Heap.back();}
|
||||
if(InlineSize<InlineCount)
|
||||
{
|
||||
T* Result=::new (static_cast<void*>(InlineData()+InlineSize)) T(std::forward<Args>(ArgsIn)...);
|
||||
++InlineSize;return *Result;
|
||||
}
|
||||
reserve(std::max<std::size_t>(InlineCount?InlineCount*2:1,InlineSize+1));
|
||||
Heap.emplace_back(std::forward<Args>(ArgsIn)...);return Heap.back();
|
||||
}
|
||||
void pop_back(){if(bHeap)Heap.pop_back();else InlineData()[--InlineSize].~T();}
|
||||
template<class It> iterator insert(iterator Position,It First,It Last)
|
||||
{
|
||||
const std::size_t Offset=static_cast<std::size_t>(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<std::size_t>(First-begin());
|
||||
const std::size_t Finish=static_cast<std::size_t>(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;I<Old;++I)data()[I-Removed]=std::move(data()[I]);
|
||||
if(bHeap)Heap.resize(Old-Removed);
|
||||
else while(InlineSize>Old-Removed)InlineData()[--InlineSize].~T();
|
||||
return begin()+Start;
|
||||
}
|
||||
|
||||
private:
|
||||
T* InlineData(){return reinterpret_cast<T*>(InlineBytes);}
|
||||
const T* InlineData()const{return reinterpret_cast<const T*>(InlineBytes);}
|
||||
void DestroyInline()noexcept{while(InlineSize>0)InlineData()[--InlineSize].~T();}
|
||||
alignas(T) std::byte InlineBytes[(InlineCount?InlineCount:1)*sizeof(T)]{};
|
||||
std::vector<T,Allocator> Heap;
|
||||
std::size_t InlineSize=0;
|
||||
bool bHeap=false;
|
||||
};
|
||||
|
||||
template<class T, class Allocator = FDefaultAllocator>
|
||||
class TArray
|
||||
{
|
||||
public:
|
||||
using value_type = T;
|
||||
using FStored = std::conditional_t<std::is_same_v<T,bool>,uint8,T>;
|
||||
using FStorage = std::vector<FStored>;
|
||||
using FStorage = FCompatSmallVector<FStored,FCompatInlineCapacity<Allocator>::Value,FCompatAllocator<FStored>>;
|
||||
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<T*>(Values.data()); }
|
||||
const T* GetData() const { return reinterpret_cast<const T*>(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<class K, class V> struct TPair { K Key{}; V Value{}; TPair()=default; TPair(const K& K0,const V& V0):Key(K0),Value(V0){}; };
|
||||
|
||||
template<class K>
|
||||
struct FCompatKeyHash
|
||||
{
|
||||
std::size_t operator()(const K& Key) const noexcept
|
||||
{
|
||||
if constexpr (std::is_integral_v<K> || std::is_enum_v<K>)
|
||||
return std::hash<K>{}(Key);
|
||||
else if constexpr (requires { GetTypeHash(Key); })
|
||||
return static_cast<std::size_t>(GetTypeHash(Key));
|
||||
else
|
||||
static_assert(sizeof(K) == 0, "Standalone TMap/TSet key type needs GetTypeHash");
|
||||
}
|
||||
};
|
||||
|
||||
template<class K, class V, class Allocator = FDefaultAllocator>
|
||||
class TMap
|
||||
{
|
||||
public:
|
||||
int32 Num() const { return static_cast<int32>(Pairs.size()); }
|
||||
bool IsEmpty() const { return Pairs.empty(); }
|
||||
void Reserve(int32 N) { Pairs.reserve(static_cast<std::size_t>(std::max(0,N))); }
|
||||
void Reset() { Pairs.clear(); }
|
||||
void Reserve(int32 N)
|
||||
{
|
||||
const size_t Count=static_cast<size_t>(std::max(0,N));
|
||||
Pairs.reserve(Count);
|
||||
const size_t Needed=std::max<size_t>(8,std::bit_ceil(std::max<size_t>(1,(Count*10+6)/7)));
|
||||
if(Buckets.size()<Needed)RebuildIndex(Needed);
|
||||
}
|
||||
void Reset()
|
||||
{
|
||||
Pairs.clear();Tombstones=0;
|
||||
if(++Generation==0){for(FBucket& B:Buckets)B.Generation=0;Generation=1;}
|
||||
}
|
||||
void Empty(int32 = 0) { Reset(); }
|
||||
bool Contains(const K& Key) const { return FindPair(Key) != Pairs.end(); }
|
||||
V* Find(const K& Key) { auto I=FindPair(Key); return I==Pairs.end()?nullptr:&I->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<size_t>(I)].Value; }
|
||||
const V* Find(const K& Key) const { const int32 I=FindIndex(Key);return I==INDEX_NONE?nullptr:&Pairs[static_cast<size_t>(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<int32>(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<int32>(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<int32>(Pairs.size()-1);
|
||||
if(I!=Last){Pairs[static_cast<size_t>(I)]=std::move(Pairs.back());UpdateIndex(Pairs[static_cast<size_t>(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<TPair<K,V>>::iterator;
|
||||
using CIt=typename std::vector<TPair<K,V>>::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<TPair<K,V>> 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<size_t>(8,std::bit_ceil(std::max<size_t>(1,(Count*10+6)/7)));
|
||||
if(Buckets.size()<Needed||Tombstones>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<K>{}(Key)&(Buckets.size()-1);
|
||||
for(size_t Probe=0;Probe<Buckets.size();++Probe)
|
||||
{
|
||||
const FBucket& B=Buckets[Slot];
|
||||
if(B.Generation!=Generation)return INDEX_NONE;
|
||||
if(B.Index>=0&&Pairs[static_cast<size_t>(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<K>{}(Key)&(Buckets.size()-1),FirstTombstone=Buckets.size();
|
||||
for(size_t Probe=0;Probe<Buckets.size();++Probe)
|
||||
{
|
||||
FBucket& B=Buckets[Slot];
|
||||
if(B.Generation!=Generation){if(FirstTombstone!=Buckets.size())Slot=FirstTombstone;Buckets[Slot]={Generation,PairIndex};return;}
|
||||
if(B.Index==INDEX_NONE&&FirstTombstone==Buckets.size())FirstTombstone=Slot;
|
||||
Slot=(Slot+1)&(Buckets.size()-1);
|
||||
}
|
||||
assert(FirstTombstone!=Buckets.size());Buckets[FirstTombstone]={Generation,PairIndex};
|
||||
}
|
||||
void MarkRemoved(const K& Key)
|
||||
{
|
||||
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1);
|
||||
for(size_t Probe=0;Probe<Buckets.size();++Probe)
|
||||
{
|
||||
FBucket& B=Buckets[Slot];if(B.Generation!=Generation)return;
|
||||
if(B.Index>=0&&Pairs[static_cast<size_t>(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<K>{}(Key)&(Buckets.size()-1);
|
||||
for(size_t Probe=0;Probe<Buckets.size();++Probe)
|
||||
{
|
||||
FBucket& B=Buckets[Slot];if(B.Generation!=Generation)return;
|
||||
if(B.Index>=0&&Pairs[static_cast<size_t>(B.Index)].Key==Key){B.Index=PairIndex;return;}
|
||||
Slot=(Slot+1)&(Buckets.size()-1);
|
||||
}
|
||||
}
|
||||
void RebuildIndex(size_t Capacity)
|
||||
{
|
||||
Capacity=std::max<size_t>(8,std::bit_ceil(Capacity));Buckets.assign(Capacity,FBucket{});Generation=1;Tombstones=0;
|
||||
for(int32 I=0;I<static_cast<int32>(Pairs.size());++I)InsertIndex(Pairs[static_cast<size_t>(I)].Key,I);
|
||||
}
|
||||
std::vector<TPair<K,V>,FCompatAllocator<TPair<K,V>>> Pairs;
|
||||
std::vector<FBucket,FCompatAllocator<FBucket>> Buckets;
|
||||
uint32 Generation=1;
|
||||
size_t Tombstones=0;
|
||||
};
|
||||
|
||||
template<class T, class Allocator = FDefaultAllocator>
|
||||
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<T,Allocator> Values;
|
||||
private:TArray<T,Allocator> Values;TMap<T,int32,Allocator> Indices;
|
||||
};
|
||||
|
||||
template<class T> class TUniquePtr
|
||||
@@ -225,6 +497,11 @@ public:
|
||||
static FString Join(const TArray<FString>& Parts, std::string_view Delimiter){FString Result;for(int32 I=0;I<Parts.Num();++I){if(I)Result+=Delimiter;Result+=Parts[I];}return Result;}
|
||||
static FString Printf(const char* Format,...){char Buf[4096];va_list A;va_start(A,Format);int N=std::vsnprintf(Buf,sizeof(Buf),Format,A);va_end(A);return N<0?FString{}:FString(Buf,static_cast<std::size_t>(std::min<int>(N,sizeof(Buf)-1)));}
|
||||
};
|
||||
inline uint32 GetTypeHash(const FString& Value)
|
||||
{
|
||||
const std::string_view View(Value.data(),Value.size());
|
||||
return static_cast<uint32>(std::hash<std::string_view>{}(View));
|
||||
}
|
||||
inline const char* operator*(const FString& S){return S.c_str();}
|
||||
using FName = FString;
|
||||
constexpr const char* NAME_None = "";
|
||||
|
||||
@@ -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.
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "WorldMesher.h"
|
||||
#include "StreamingWorld.h"
|
||||
#include "StandaloneResearchTunnels.h"
|
||||
|
||||
#ifdef TEXT
|
||||
@@ -25,6 +25,8 @@
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#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<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;
|
||||
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<FStandaloneOwnerWorld> World;
|
||||
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
|
||||
std::string DataPath;
|
||||
std::mutex ReadyMutex;
|
||||
std::vector<FReadyLevel> ReadyLevels;
|
||||
std::thread GenerationThread;
|
||||
std::atomic<bool> GenerationDone{false};
|
||||
std::atomic<bool> CancelGeneration{false};
|
||||
FStandaloneCpuClipmap LastClipmap;
|
||||
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]{};
|
||||
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;
|
||||
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<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;
|
||||
@@ -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<std::mutex> 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<FStandaloneStreamingGenerator>(*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<uint32>(Seed)+static_cast<uint32>(Delta);
|
||||
Seed=static_cast<int32>(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<uint64>(Mesh.Indices.size());
|
||||
if(Mesh.Vertices.empty()||Mesh.Indices.empty()){L.Ready=true;return true;}
|
||||
if(Mesh.Indices.size()>std::numeric_limits<UINT>::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<UINT>(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<unsigned long long>(L.VertexCount),L.Seconds,static_cast<unsigned long long>(L.Tiles));
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
Times.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void PumpGeneration()
|
||||
{
|
||||
std::vector<FReadyLevel> Ready;
|
||||
{std::lock_guard<std::mutex> 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<FStandaloneCpuTile> 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<unsigned long long>(TotalVertices),static_cast<unsigned long long>(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<unsigned long long>(LastClipmap.Levels[L].TileCount),
|
||||
static_cast<unsigned long long>(LastClipmap.Levels[L].VertexCount),
|
||||
static_cast<double>(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<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);
|
||||
}
|
||||
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<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)
|
||||
@@ -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 "<<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";
|
||||
@@ -392,10 +705,7 @@ float4 PS(O i):SV_TARGET{
|
||||
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(bGenerationRunning&&NeedsRecenter(BuildTargetChunk))
|
||||
{bRebuildPending=true;CancelGeneration.store(true,std::memory_order_relaxed);}
|
||||
else if(!bGenerationRunning&&World&&NeedsRecenter(LastClipmap.CenterChunk))
|
||||
{bRebuildPending=true;}
|
||||
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);
|
||||
@@ -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<int32>(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<Argc;++I)
|
||||
{
|
||||
const std::string_view Arg=Argv[I];
|
||||
if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10));
|
||||
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;}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
#pragma once
|
||||
|
||||
#include "WorldMesher.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <unordered_set>
|
||||
|
||||
struct FStandaloneTileKeyHash
|
||||
{
|
||||
std::size_t operator()(const FVoxelTileKey& Key) const noexcept
|
||||
{ return static_cast<std::size_t>(GetTypeHash(Key)); }
|
||||
};
|
||||
|
||||
struct FStandaloneDesiredTileSet
|
||||
{
|
||||
std::vector<FVoxelTileKey> 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<FStandaloneCpuVertex> Vertices;
|
||||
std::vector<uint32> 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<double>(1<<Tile.Level);
|
||||
const double DX=(static_cast<double>(Tile.Coord.X)+0.5)*Scale-Center.X;
|
||||
const double DY=(static_cast<double>(Tile.Coord.Y)+0.5)*Scale-Center.Y;
|
||||
const double DZ=(static_cast<double>(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<FVoxelTileKey> Tiles;
|
||||
VoxelClipmapDesiredTiles::Build(Desired.CenterChunk,Desired.MinimumChunkZ,
|
||||
Desired.MaximumChunkZ,Parameters,Tiles);
|
||||
Desired.Tiles.reserve(static_cast<size_t>(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<B.Level:DA<DB;
|
||||
});
|
||||
return Desired;
|
||||
}
|
||||
}
|
||||
|
||||
class FStandaloneStreamingGenerator
|
||||
{
|
||||
public:
|
||||
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=6)
|
||||
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,32))
|
||||
{
|
||||
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
||||
}
|
||||
FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete;
|
||||
FStandaloneStreamingGenerator& operator=(const FStandaloneStreamingGenerator&)=delete;
|
||||
~FStandaloneStreamingGenerator(){Stop();}
|
||||
|
||||
FStandaloneStreamingUpdate Start(const FVector& CameraPositionVoxels,
|
||||
const std::vector<FVoxelTileKey>& ResidentKeys={},
|
||||
uint64 CrossingIndex=0)
|
||||
{
|
||||
Stop();
|
||||
{
|
||||
std::lock_guard<std::mutex> Lock(Mutex);
|
||||
bStop=false;
|
||||
}
|
||||
for(int32 I=0;I<WorkerCount;++I)Workers.emplace_back([this](){WorkerLoop();});
|
||||
return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex);
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> Lock(Mutex);
|
||||
bStop=true;
|
||||
Queue.clear();
|
||||
}
|
||||
Wake.notify_all();
|
||||
for(std::thread& Worker:Workers)if(Worker.joinable())Worker.join();
|
||||
Workers.clear();
|
||||
std::lock_guard<std::mutex> Lock(Mutex);
|
||||
Ready.clear();Outstanding.clear();Desired.clear();
|
||||
}
|
||||
|
||||
FStandaloneStreamingUpdate UpdateDesired(
|
||||
const FVector& CameraPositionVoxels,const std::vector<FVoxelTileKey>& ResidentKeys,
|
||||
uint64 CrossingIndex=0)
|
||||
{
|
||||
FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels);
|
||||
FStandaloneStreamingUpdate Stats;
|
||||
Stats.CenterChunk=NewDesired.CenterChunk;
|
||||
Stats.DesiredTiles=static_cast<int32>(NewDesired.Tiles.size());
|
||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Resident;
|
||||
Resident.reserve(ResidentKeys.size()*2+1);
|
||||
for(const FVoxelTileKey& Key:ResidentKeys)Resident.insert(Key);
|
||||
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
||||
{
|
||||
if(Key.Level<World->FullResClipLevels)++Stats.FullResTiles;
|
||||
if(Resident.contains(Key))++Stats.RetainedResidentTiles;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> 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<B.Key.Level:DA<DB;
|
||||
});
|
||||
Wake.notify_all();
|
||||
return Stats;
|
||||
}
|
||||
|
||||
std::vector<FStandaloneCpuTile> TakeReady()
|
||||
{
|
||||
std::vector<FStandaloneCpuTile> Out;
|
||||
std::lock_guard<std::mutex> 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<std::mutex> Lock(Mutex);return Desired.contains(Key);}
|
||||
int32 DesiredCount()const
|
||||
{std::lock_guard<std::mutex> Lock(Mutex);return static_cast<int32>(Desired.size());}
|
||||
int32 FullResDesiredCount()const
|
||||
{std::lock_guard<std::mutex> Lock(Mutex);return FullResTileCount;}
|
||||
FIntVector CenterChunk()const
|
||||
{std::lock_guard<std::mutex> 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<float>(World->SkirtCells);
|
||||
Mesher.LODOctaveDrop=World->LODOctaveDrop;
|
||||
const int32 Level=Job.Key.Level;
|
||||
const int32 Extent=Job.Key.ExtentVoxels();
|
||||
const int32 Cells=Level<World->FullResClipLevels?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<uint64>(FMath::Max<int64>(0,SampleCount));
|
||||
Out.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
|
||||
for(int32 I=0;I<Mesh.Vertices.Num();++I)
|
||||
{
|
||||
const FVector& P=Mesh.Vertices[I];const FVector& N=Mesh.Normals[I];
|
||||
Out.Vertices.push_back({
|
||||
{static_cast<float>(P.X*0.01),static_cast<float>(P.Y*0.01),static_cast<float>(P.Z*0.01)},
|
||||
{static_cast<float>(N.X),static_cast<float>(N.Y),static_cast<float>(N.Z)}});
|
||||
}
|
||||
Out.Indices.reserve(static_cast<size_t>(Mesh.Triangles.Num()));
|
||||
for(const int32 Index:Mesh.Triangles)Out.Indices.push_back(static_cast<uint32>(Index));
|
||||
Out.WorkerSeconds=std::chrono::duration<double>(FClock::now()-StartTime).count();
|
||||
return Out;
|
||||
}
|
||||
|
||||
void WorkerLoop()
|
||||
{
|
||||
for(;;)
|
||||
{
|
||||
FStandaloneTileJob Job;
|
||||
{
|
||||
std::unique_lock<std::mutex> 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<std::mutex> 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<std::thread> Workers;
|
||||
std::vector<FStandaloneTileJob> Queue;
|
||||
std::vector<FStandaloneCpuTile> Ready;
|
||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Desired;
|
||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Outstanding;
|
||||
FStandaloneDesiredTileSet CurrentDesired;
|
||||
int32 FullResTileCount=0;
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
Reference in new issue
Block a user