standalone: per-tile speed at plugin level, streaming like the game, softer lighting
Cause found by profiling: room/SDF graph-cache churn (exact XY bounds + sample order rebuilt it constantly). Region grouping (one LOD3 tile: 1,084 -> 229 builds), taller-tile traversal for LOD6 (36,480 -> 12,160), extent-dependent cache slots, lambdas instead of recursive std::function. Serial ms/tile, 64 nearest tiles per level (before -> after): L0 50.4 -> 50.6, L1 112 -> 110, L2 24 -> 22, L3 223 -> 73, L4 269 -> 134, L5 393 -> 127, L6 283 -> 145. Hashes of all levels unchanged; parity 3,436/3,436 bit-identical. Full clipmap, 16 workers BelowNormal: full detail 66.6 s, all 12,054 tiles 175 s. In-stream L0 tail is high (median 83 ms, mean 198 ms, p95 669 ms) vs 51 ms serial. Streaming: recentre keeps the intersection, generates only missing tiles nearest/finest first; one crossing = exactly 441 new tiles in 6.1 s, no stale results. Lighting: normals already come from the density gradient; the striping was the squared headlamp; broad linear fill + warmer palette. Daytime CPU rule documented in Standalone/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
No files matched your search
@@ -28,3 +28,5 @@
|
|||||||
|
|
||||||
# Keep every design / doc markdown at any depth
|
# Keep every design / doc markdown at any depth
|
||||||
!*.md
|
!*.md
|
||||||
|
/Standalone/data/*.log
|
||||||
|
/Standalone/data/*.err
|
||||||
@@ -54,7 +54,7 @@ $RspLines += @('/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gd
|
|||||||
|
|
||||||
Push-Location $BuildDir
|
Push-Location $BuildDir
|
||||||
try {
|
try {
|
||||||
$CommandLine = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath
|
$CommandLine = 'call "{0}" >nul && start /belownormal /wait cl.exe @"{1}"' -f $VcVars,$ResponsePath
|
||||||
& $env:ComSpec /d /s /c $CommandLine
|
& $env:ComSpec /d /s /c $CommandLine
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Standalone MSVC build failed with exit code $LASTEXITCODE" }
|
if ($LASTEXITCODE -ne 0) { throw "Standalone MSVC build failed with exit code $LASTEXITCODE" }
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
#include <bit>
|
#include <bit>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <charconv>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <immintrin.h>
|
#include <immintrin.h>
|
||||||
@@ -632,8 +634,73 @@ struct FWriteScopeLock { explicit FWriteScopeLock(FRWLock&L):Lock(L.Mutex){} std
|
|||||||
enum EConsoleVariableFlags { ECVF_Default=0, ECVF_SetByCommandline=1, ECVF_SetByCode=2 };
|
enum EConsoleVariableFlags { ECVF_Default=0, ECVF_SetByCommandline=1, ECVF_SetByCode=2 };
|
||||||
class FAutoConsoleVariableRef { public:FAutoConsoleVariableRef(const char*,int32&,const char*,uint32=0){} FAutoConsoleVariableRef(const char*,float&,const char*,uint32=0){} FAutoConsoleVariableRef(const char*,bool&,const char*,uint32=0){} };
|
class FAutoConsoleVariableRef { public:FAutoConsoleVariableRef(const char*,int32&,const char*,uint32=0){} FAutoConsoleVariableRef(const char*,float&,const char*,uint32=0){} FAutoConsoleVariableRef(const char*,bool&,const char*,uint32=0){} };
|
||||||
template<class T> class TAutoConsoleVariable { public:TAutoConsoleVariable(const char*,T Default,const char*,uint32=0):Value(Default){}T GetValueOnAnyThread()const{return Value.load();}T GetValueOnGameThread()const{return Value.load();}void Set(T V,uint32=0){Value.store(V);} TAutoConsoleVariable*operator->(){return this;}private:std::atomic<T>Value;};
|
template<class T> class TAutoConsoleVariable { public:TAutoConsoleVariable(const char*,T Default,const char*,uint32=0):Value(Default){}T GetValueOnAnyThread()const{return Value.load();}T GetValueOnGameThread()const{return Value.load();}void Set(T V,uint32=0){Value.store(V);} TAutoConsoleVariable*operator->(){return this;}private:std::atomic<T>Value;};
|
||||||
struct FCommandLine { static const char* Get(){return "";} };
|
struct FCommandLine
|
||||||
struct FParse { static bool Value(const char*,const char*,int32&){return false;} static bool Value(const char*,const char*,float&){return false;} static bool Value(const char*,const char*,FString&){return false;} static bool Param(const char*,const char*){return false;} };
|
{
|
||||||
|
inline static std::string Arguments;
|
||||||
|
static const char* Get(){return Arguments.c_str();}
|
||||||
|
static void Set(std::string_view InArguments){Arguments.assign(InArguments);}
|
||||||
|
};
|
||||||
|
struct FParse
|
||||||
|
{
|
||||||
|
static bool ReadValue(const char* CommandLine,const char* Key,std::string& Out)
|
||||||
|
{
|
||||||
|
if(!CommandLine||!Key||!*Key)return false;
|
||||||
|
const std::string_view Command(CommandLine),Token(Key);
|
||||||
|
std::size_t Position=0;
|
||||||
|
while((Position=Command.find(Token,Position))!=std::string_view::npos)
|
||||||
|
{
|
||||||
|
const bool bTokenStart=Position==0||Command[Position-1]==' '
|
||||||
|
||Command[Position-1]=='\t'||Command[Position-1]=='-';
|
||||||
|
const std::size_t ValueStart=Position+Token.size();
|
||||||
|
if(bTokenStart&&ValueStart<=Command.size())
|
||||||
|
{
|
||||||
|
std::size_t ValueEnd=Command.find_first_of(" \t\r\n",ValueStart);
|
||||||
|
if(ValueEnd==std::string_view::npos)ValueEnd=Command.size();
|
||||||
|
if(ValueEnd>ValueStart)
|
||||||
|
{Out.assign(Command.substr(ValueStart,ValueEnd-ValueStart));return true;}
|
||||||
|
}
|
||||||
|
Position+=Token.size();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static bool Value(const char* CommandLine,const char* Key,int32& Out)
|
||||||
|
{
|
||||||
|
std::string Text;
|
||||||
|
if(!ReadValue(CommandLine,Key,Text))return false;
|
||||||
|
const auto Result=std::from_chars(Text.data(),Text.data()+Text.size(),Out);
|
||||||
|
return Result.ec==std::errc{}&&Result.ptr==Text.data()+Text.size();
|
||||||
|
}
|
||||||
|
static bool Value(const char* CommandLine,const char* Key,float& Out)
|
||||||
|
{
|
||||||
|
std::string Text;
|
||||||
|
if(!ReadValue(CommandLine,Key,Text))return false;
|
||||||
|
char* End=nullptr;Out=std::strtof(Text.c_str(),&End);
|
||||||
|
return End==Text.c_str()+Text.size();
|
||||||
|
}
|
||||||
|
static bool Value(const char* CommandLine,const char* Key,FString& Out)
|
||||||
|
{
|
||||||
|
std::string Text;
|
||||||
|
if(!ReadValue(CommandLine,Key,Text))return false;
|
||||||
|
Out=FString(Text);return true;
|
||||||
|
}
|
||||||
|
static bool Param(const char* CommandLine,const char* Name)
|
||||||
|
{
|
||||||
|
if(!CommandLine||!Name||!*Name)return false;
|
||||||
|
const std::string_view Command(CommandLine),Token(Name);
|
||||||
|
std::size_t Position=0;
|
||||||
|
while((Position=Command.find(Token,Position))!=std::string_view::npos)
|
||||||
|
{
|
||||||
|
const bool bTokenStart=Position==0||Command[Position-1]==' '
|
||||||
|
||Command[Position-1]=='\t'||Command[Position-1]=='-';
|
||||||
|
const std::size_t End=Position+Token.size();
|
||||||
|
const bool bTokenEnd=End==Command.size()||Command[End]==' '
|
||||||
|
||Command[End]=='\t'||Command[End]=='\r'||Command[End]=='\n';
|
||||||
|
if(bTokenStart&&bTokenEnd)return true;
|
||||||
|
Position=End;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
|
struct FCrc { static uint32 MemCrc32(const void*Data,int32 Length,uint32 Crc=0){const auto*P=static_cast<const uint8*>(Data);uint32 V=~Crc;for(int32 I=0;I<Length;++I){V^=P[I];for(int J=0;J<8;++J)V=(V>>1)^(0xEDB88320u&static_cast<uint32>(-static_cast<int32>(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast<int32>(std::strlen(S)),Crc);} };
|
||||||
template<class T> using TAtomic = std::atomic<T>;
|
template<class T> using TAtomic = std::atomic<T>;
|
||||||
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
|
class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomic<int32>Value{0};};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# VoxelForge standalone world
|
# VoxelForge standalone world
|
||||||
|
|
||||||
This C++20 program loads the owner snapshot from `data/owner_world.json`, runs the copied
|
This C++20 program loads the owner snapshot from `data/owner_world.json`, runs the copied
|
||||||
VoxelForge density pipeline and marching-cubes mesher, builds all clipmap levels with six worker
|
VoxelForge density pipeline and marching-cubes mesher, streams the clipmap with six worker threads
|
||||||
threads, and displays them through Direct3D 11. Unreal is not linked or needed at run time.
|
by default, and displays it through Direct3D 11. Unreal is not linked or needed at run time.
|
||||||
|
|
||||||
## Build and launch
|
## Build and launch
|
||||||
|
|
||||||
@@ -14,7 +14,18 @@ From the plugin root, run:
|
|||||||
```
|
```
|
||||||
|
|
||||||
The build uses the configured VS 2022 `vcvars64.bat`, MSVC `/std:c++20 /O2 /arch:AVX2 /fp:precise`.
|
The build uses the configured VS 2022 `vcvars64.bat`, MSVC `/std:c++20 /O2 /arch:AVX2 /fp:precise`.
|
||||||
The executable finds the JSON beside itself under `Standalone\data\`.
|
`Build.ps1` starts MSVC at BelowNormal priority and does not enable `/MP`.
|
||||||
|
Pass another executable name to build beside any viewer that may be open:
|
||||||
|
`.\Standalone\Build.ps1 -OutputName VoxelForgeStandalone_test.exe`. The executable finds the
|
||||||
|
JSON beside itself under `Standalone\data\`.
|
||||||
|
|
||||||
|
### Daytime resource limit
|
||||||
|
|
||||||
|
Run tests and benchmarks at Idle or BelowNormal priority and use no more than 8 of this PC's 16
|
||||||
|
logical CPUs. The default viewer uses six workers. Use `Build\LaunchBelowNormal.ps1` for any test or
|
||||||
|
benchmark process; it starts the child at BelowNormal and verifies the priority. Builds use no more
|
||||||
|
than four compiler processes (the current build is serial) and retain `/fp:precise`. The all-core
|
||||||
|
16-worker runs documented below were the owner's one-night exception.
|
||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
@@ -23,8 +34,9 @@ The executable finds the JSON beside itself under `Standalone\data\`.
|
|||||||
- F toggles wireframe.
|
- F toggles wireframe.
|
||||||
- 1 through 7 toggle clipmap levels 0 through 6 individually; 0 toggles every level.
|
- 1 through 7 toggle clipmap levels 0 through 6 individually; 0 toggles every level.
|
||||||
- N/P changes the seed. T switches between the owner's base tunnels and the research-shaped round,
|
- N/P changes the seed. T switches between the owner's base tunnels and the research-shaped round,
|
||||||
winding tunnel option. Either action rebuilds the clipmap. Moving three chunks from its center
|
winding tunnel option; `--round-tunnels` starts in that variant. Either action rebuilds the clipmap.
|
||||||
also recenters and rebuilds it.
|
Crossing a chunk recenters streaming:
|
||||||
|
tiles still desired stay resident, only new tiles are generated, and leaving tiles are dropped.
|
||||||
- Esc exits.
|
- Esc exits.
|
||||||
|
|
||||||
The console reports density parity at startup, each level's generation time, and the total time.
|
The console reports density parity at startup, each level's generation time, and the total time.
|
||||||
@@ -50,10 +62,11 @@ and seed. The export also records their geometry in JSON for reference.
|
|||||||
- Standalone `VOXEL_SIZE` is 25 cm; the plugin's checked-out value remains 12.5 cm. No other voxel
|
- Standalone `VOXEL_SIZE` is 25 cm; the plugin's checked-out value remains 12.5 cm. No other voxel
|
||||||
conversion constant needed changing. Mesh vertices stay in centimetres in the copied mesher and
|
conversion constant needed changing. Mesh vertices stay in centimetres in the copied mesher and
|
||||||
the viewer converts them to metres at upload.
|
the viewer converts them to metres at upload.
|
||||||
- The compatibility `TMap` and `TSet` are vector-backed containers, so lookup is linear and their
|
- The compatibility `TMap` uses an open-addressed hash index with stable pair storage;
|
||||||
iteration follows insertion order rather than Unreal hash-table order. Owner strate slots and
|
`TSet` uses that index for lookup. Iteration follows insertion order rather than Unreal hash-table
|
||||||
passages are read from ordered JSON arrays. Perlin noise and deterministic `DetSinCos` are copied
|
order. Owner strate slots and passages are read from ordered JSON arrays. Perlin noise and
|
||||||
unchanged; MSVC uses `/fp:precise` without fast-math/FMA contraction.
|
deterministic `DetSinCos` are copied unchanged; MSVC uses `/fp:precise` without fast-math/FMA
|
||||||
|
contraction.
|
||||||
- Unreal reflection and UObject construction are replaced by explicit JSON-to-holder mapping.
|
- Unreal reflection and UObject construction are replaced by explicit JSON-to-holder mapping.
|
||||||
Density-relevant settings, resolved strate layouts, passages, biome parameters, and terrain-op
|
Density-relevant settings, resolved strate layouts, passages, biome parameters, and terrain-op
|
||||||
assets are loaded. Runtime actor/material assets are not instantiated; the viewer uses one lit
|
assets are loaded. Runtime actor/material assets are not instantiated; the viewer uses one lit
|
||||||
@@ -98,3 +111,136 @@ comparing later speed changes.
|
|||||||
| 5 | 23.809 | 294 | 199,503 | 2.02 M |
|
| 5 | 23.809 | 294 | 199,503 | 2.02 M |
|
||||||
| 6 | 296.304 | 4,704 | 4,032,866 | 32.26 M |
|
| 6 | 296.304 | 4,704 | 4,032,866 | 32.26 M |
|
||||||
| **Total** | **515.117** | **12,054** | **6,605,189** | **253.86 M** |
|
| **Total** | **515.117** | **12,054** | **6,605,189** | **253.86 M** |
|
||||||
|
|
||||||
|
## Profiling result and speed change
|
||||||
|
|
||||||
|
The compatibility layer's old `TMap`/`TSet` linear lookups and heap-only vector growth were
|
||||||
|
replaced with an open-addressed hash index, stable pair storage, small inline storage, and a
|
||||||
|
thread-local pooled allocator. That removes known container and allocation mismatches with the
|
||||||
|
Unreal path, but did not improve the earlier LOD0 timing (72.1 s before, 74.1 s after); profiling
|
||||||
|
showed those were not the dominant cost. The same audit also found two recursive room-graph walks
|
||||||
|
wrapped in `std::function`, which could allocate and dispatch indirectly on every graph build. They
|
||||||
|
now use self-recursive generic lambdas with the same depth-first visit order, removing that wrapper
|
||||||
|
overhead from the generation hot path. In matched profile runs, RoomGraphBuild fell from 4.531 to
|
||||||
|
4.417 s for 64 LOD5 tiles and from 5.210 to 5.038 s for 64 LOD6 tiles; graph build counts and
|
||||||
|
density hashes were unchanged.
|
||||||
|
|
||||||
|
Per-stage timers and cache counters found the real hotspot in `GetDensityAt`: the room/SDF graph
|
||||||
|
cache is keyed by exact XY bounds, and the original X-fast lattice walk kept rebuilding it. On one
|
||||||
|
LOD3 tile, cache-region grouping cut graph builds from 1,084 to 229 while writing samples to their
|
||||||
|
original lattice indices. A 64-tile LOD6 profile attributed 13.189 s of 19.874 s to 36,480 room
|
||||||
|
graph builds. For tiles wider than 16 chunks, keeping each XY column's Z run contiguous reduced
|
||||||
|
that to 12,160 builds and 5.210 s of 10.972 s, with the same lattice hash. The final recursive-
|
||||||
|
lambda profile measured 5.038 s in room-graph builds and 10.600 s total for the same 64 LOD6 tiles.
|
||||||
|
Finer tiles retain the plane-first traversal, which is faster for their shorter cache runs.
|
||||||
|
|
||||||
|
Cache-slot experiments found a second scale-specific case: 32-chunk LOD5 tiles improve with two
|
||||||
|
room graph cache slots (26,926 to 9,700 builds; profiled RoomGraphBuild 10.151 to 4.475 s across
|
||||||
|
64 tiles). Two slots regress 64-chunk LOD6 tiles (12,160 to 23,104 builds), so the generator uses
|
||||||
|
two only for 32-chunk tile contexts and one otherwise. The selected-tile density hash is unchanged.
|
||||||
|
|
||||||
|
The controlled before/after run below meshes the 64 nearest tiles for each LOD serially, without
|
||||||
|
profiling or hash collection. It uses the same owner seed and camera-centered desired set. The
|
||||||
|
selected-tile lattice hashes from a separate final run are identical at every LOD.
|
||||||
|
|
||||||
|
| LOD | 64-tile density hash |
|
||||||
|
|---:|---:|
|
||||||
|
| 0 | `0x3d31a090650399d3` |
|
||||||
|
| 1 | `0x6150c607c7a5c76f` |
|
||||||
|
| 2 | `0x9f1d560bc14e67f1` |
|
||||||
|
| 3 | `0xdd3e76020ce69cd7` |
|
||||||
|
| 4 | `0x59a209a5db034b46` |
|
||||||
|
| 5 | `0x1fdeb60160e4216f` |
|
||||||
|
| 6 | `0xbac69461f812493d` |
|
||||||
|
|
||||||
|
| LOD | Before ms/tile | After ms/tile | After p50 | After p95 |
|
||||||
|
|---:|---:|---:|---:|---:|
|
||||||
|
| 0 | 50.4 | 50.551 | 50.521 | 63.148 |
|
||||||
|
| 1 | 112.3 | 110.175 | 33.030 | 407.767 |
|
||||||
|
| 2 | 23.9 | 22.430 | 12.015 | 51.078 |
|
||||||
|
| 3 | 223.0 | 73.482 | 69.614 | 94.331 |
|
||||||
|
| 4 | 269.0 | 133.512 | 98.003 | 214.854 |
|
||||||
|
| 5 | 393.1 | 127.325 | 116.386 | 173.401 |
|
||||||
|
| 6 | 282.8 | 145.457 | 140.952 | 183.234 |
|
||||||
|
|
||||||
|
Run mode: BelowNormal priority, serial headless benchmark, 64 nearest tiles per LOD, profiling and
|
||||||
|
hash collection off; final executable `VoxelForgeStandalone_night20.exe`. The before timings are
|
||||||
|
from the earlier 64-tile baseline. The user's prior plugin reference is about 95 ms worker time for
|
||||||
|
a 25 cm LOD0 tile. The final nearest-tile LOD0 average is 50.6 ms. In the complete streamed run,
|
||||||
|
LOD0 median was 82.9 ms, but its mean was 197.6 ms and p95 was 668.8 ms. The plugin was not timed
|
||||||
|
on the same tile coordinates, so the rough reference comparison is not apples-to-apples and the
|
||||||
|
streamed tail does not meet that target yet.
|
||||||
|
|
||||||
|
With 16 BelowNormal workers, the final full run completed all 12,054 tiles in 175.056 s, down from
|
||||||
|
the historical 515.117 s six-worker run. It reached full detail after 66.565 s. Worker time per
|
||||||
|
level in the final run was:
|
||||||
|
|
||||||
|
| LOD | Worker sum | Tiles | Avg/tile | p50 | p95 |
|
||||||
|
|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 0 | 542.284 s | 2,744 | 197.625 ms | 82.946 ms | 668.796 ms |
|
||||||
|
| 1 | 501.518 s | 2,009 | 249.635 ms | 89.990 ms | 742.258 ms |
|
||||||
|
| 2 | 68.712 s | 1,274 | 53.934 ms | 55.633 ms | 120.102 ms |
|
||||||
|
| 3 | 77.055 s | 588 | 131.047 ms | 122.697 ms | 212.157 ms |
|
||||||
|
| 4 | 103.552 s | 441 | 234.812 ms | 192.137 ms | 418.249 ms |
|
||||||
|
| 5 | 66.689 s | 294 | 226.834 ms | 223.139 ms | 328.030 ms |
|
||||||
|
| 6 | 1,439.125 s | 4,704 | 305.936 ms | 303.062 ms | 391.440 ms |
|
||||||
|
|
||||||
|
The worker sums are concurrent wall-time measurements and therefore add to more than the 175.056 s
|
||||||
|
clipmap wall time. The historical and final full runs used different worker counts; the serial
|
||||||
|
64-tile table is the controlled per-tile comparison. The full run also passed density parity at
|
||||||
|
3,436/3,436 samples with zero mismatches.
|
||||||
|
|
||||||
|
## Streaming and uploads
|
||||||
|
|
||||||
|
The viewer rebuilds the desired tile set at each chunk crossing, keeps the intersection resident,
|
||||||
|
cancels queued jobs that left the set, and enqueues only missing tiles. A heap orders by camera
|
||||||
|
distance, then prefers finer LOD at equal distance. Workers generate CPU meshes concurrently. D3D11
|
||||||
|
uploads are limited to four batches or 2 ms per frame. A leaving or changed batch keeps its prior GPU
|
||||||
|
buffer until its replacement is uploaded, avoiding a hole while the batch is rebuilt.
|
||||||
|
|
||||||
|
The final 16-worker run reached full detail 66.565 s after spawn (4,753 LOD0–1 desired tiles; 5,173
|
||||||
|
resident through LOD1) and the full clipmap at 175.056 s. In a controlled one-chunk crossing on the
|
||||||
|
same executable, 4,856 tiles stayed resident, 441 left, and only 441 new jobs were submitted. Those
|
||||||
|
jobs covered 18.91 M density samples, used 88.573 worker-seconds, and finished in 6.108 s wall time;
|
||||||
|
all 441 were generated with no stale results or queued cancellations. The upload budget remained
|
||||||
|
four batches / 2 ms per frame.
|
||||||
|
|
||||||
|
For a deterministic movement check, `--test-one-chunk-crossing` moves once after full-detail
|
||||||
|
readiness and closes the test viewer after that crossing's jobs and bounded uploads drain.
|
||||||
|
|
||||||
|
## Lighting comparison
|
||||||
|
|
||||||
|
The copied plugin mesher already computes central-difference density-gradient normals on the sampled
|
||||||
|
density lattice and interpolates them along marching-cubes edges. The standalone passes those
|
||||||
|
normals through to D3D11, so replacing face normals was unnecessary. Profiling measured this existing
|
||||||
|
gradient stage at 2.575 ms across 64 LOD0 benchmark tiles (about 72 ms over all 2,744 streamed LOD0
|
||||||
|
tiles); this round adds no density samples or meshing work for normals.
|
||||||
|
|
||||||
|
The visible striping was amplified by the viewer's squared, high-intensity camera-facing light. The
|
||||||
|
shader now uses a broad linear camera fill at similar average strength. Geometry, density, and LOD
|
||||||
|
are unchanged; the shading path uses fewer multiplications. The D3D11 pixel-shader adjustment has
|
||||||
|
no CPU mesh-generation cost.
|
||||||
|
|
||||||
|
Both captures use the default seed, default camera, 1280x800 view, and are saved from the D3D11 back
|
||||||
|
buffer after all full-detail tiles upload:
|
||||||
|
|
||||||
|
| Before | After |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
## Tunnel palette pass
|
||||||
|
|
||||||
|
As a separate, render-only visual pass, the neutral blue-gray rock constants were changed to warm
|
||||||
|
limestone and ochre, with a softer cyan camera fill. This follows the color balance of the approved
|
||||||
|
cave reference while leaving its mesh, 25 cm resolution, and detail untouched. The shader changes
|
||||||
|
only existing constant values and adds no texture reads or operations; CPU density and meshing cost is
|
||||||
|
zero. The matched 16-worker captures generated the same 7,521,175 vertices and 41,506,623 indices;
|
||||||
|
full-clipmap wall times were 172.772 s before and 171.676 s after, within run variance.
|
||||||
|
|
||||||
|
Both images show seed 0 with the round/winding tunnel field, at 1280x800 after the full clipmap
|
||||||
|
uploaded. Camera: `(-11.75, -123.25, -19.85)` m, yaw 90 degrees, pitch 0. Capture mode used 16
|
||||||
|
BelowNormal workers:
|
||||||
|
|
||||||
|
| Before palette pass | After palette pass |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
@@ -1,30 +1,74 @@
|
|||||||
# Shared C++ library plan
|
# Shared C++ library plan
|
||||||
|
|
||||||
|
This round keeps the standalone copy in place. The next extraction should make one implementation
|
||||||
|
serve both hosts; it should not turn the compatibility layer into a second public API.
|
||||||
|
|
||||||
## Library contents
|
## Library contents
|
||||||
|
|
||||||
Move the deterministic CPU world pipeline into a portable `VoxelForgeCore` library: plain voxel
|
Move the deterministic CPU pipeline into `VoxelForgeCore`:
|
||||||
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,
|
- Plain voxel, tile, vector, bounds, and resolved-world input types.
|
||||||
asset/reflection access, gameplay actors, and task scheduling in the host applications.
|
- Deterministic math, noise, density operations, cave and passage fields, strate composition, and
|
||||||
|
the room/SDF caches they use.
|
||||||
|
- Marching-cubes tables and meshing, including the density-lattice gradient normals.
|
||||||
|
- Clipmap desired-set calculation and the shared density evaluation entry points.
|
||||||
|
|
||||||
|
Keep JSON and Unreal asset loading, material/actor creation, D3D11 and Unreal rendering, collision,
|
||||||
|
worker scheduling, cancellation policy, and per-frame mesh upload budgets in the host applications.
|
||||||
|
The cache-region traversal found in this round belongs with the density/meshing implementation.
|
||||||
|
The worker-local room graph cache is keyed by exact XY bounds and stratum. Grouping samples by the
|
||||||
|
legacy cache regions reduced a profiled LOD3 tile from 1,084 graph builds to 229. At coarser sizes,
|
||||||
|
plane-first traversal still thrashes the same XY windows across a tall Z run, so tiles wider than 16
|
||||||
|
chunks keep each XY column's Z samples contiguous. The profiled 64-tile LOD6 case fell from 36,480
|
||||||
|
graph builds (13.189 s in graph construction) to 12,160 (5.210 s), with the same density hash.
|
||||||
|
|
||||||
|
Cache capacity is also scale-dependent: a 32-chunk LOD5 tile benefits from two cached stratum
|
||||||
|
windows (26,926 to 9,700 builds across 64 tiles), while a 64-chunk LOD6 tile regresses with two
|
||||||
|
slots (12,160 to 23,104), so the current generator selects two only for 32-chunk tile contexts and
|
||||||
|
one otherwise. Replacing the room-graph `std::function` recursion then reduced profiled LOD5
|
||||||
|
RoomGraphBuild from 4.531 to 4.417 s and LOD6 from 5.210 to 5.038 s across the same 64 tiles,
|
||||||
|
without changing graph build counts or density hashes. These timings are profiling evidence, not a
|
||||||
|
cross-host benchmark. The shared API
|
||||||
|
should own the traversal and cache policy, with parity tests covering both traversal paths while
|
||||||
|
writing the same original density-grid indices.
|
||||||
|
|
||||||
|
The compatibility-map and allocator change did not move the original LOD0 run; those changes remove
|
||||||
|
known linear-lookup and heap-growth gaps, but stage profiling put the cost in density evaluation and
|
||||||
|
room-graph cache churn. The hot room-graph walks also used an owning `std::function` compatibility
|
||||||
|
alias for recursion; replacing them with generic self-recursive lambdas kept the same depth-first
|
||||||
|
order and removed repeated type-erasure/allocation overhead. Keep that code in the core rather than
|
||||||
|
carrying the UE-style callback alias into it. Viewer lighting and its palette remain host renderer
|
||||||
|
choices and stay outside the shared generator.
|
||||||
|
|
||||||
## Compatibility boundary
|
## Compatibility boundary
|
||||||
|
|
||||||
The library should expose standard C++ value types and explicit immutable world inputs. Unreal
|
The core should expose standard C++ value types, explicit immutable resolved inputs, and owned mesh
|
||||||
adapters convert `UObject` settings/assets into those inputs and convert mesh results back to
|
results. No Unreal headers, reflection macros, `UObject`, Unreal containers, renderer types, or host
|
||||||
Unreal types. The standalone loader maps its JSON snapshot into the same inputs. No Unreal headers,
|
allocator should cross the boundary. The plugin adapter converts settings and loaded assets into
|
||||||
reflection macros, `UObject`, or host allocator should cross the library boundary.
|
the resolved input snapshot, then converts the returned vertices, indices, and normals into Unreal
|
||||||
|
mesh types. The standalone JSON loader maps its exported snapshot into the same input snapshot.
|
||||||
|
|
||||||
|
Do not move scheduling into the library. Each host should pass an immutable generation context to a
|
||||||
|
worker and receive an owned result; this lets the plugin retain its task admission and game-thread
|
||||||
|
rules while the viewer retains its worker pool and bounded D3D11 upload pump.
|
||||||
|
|
||||||
## Plugin integration
|
## Plugin integration
|
||||||
|
|
||||||
Build the core as one C++20 static library and link it from both the standalone viewer and the
|
Add a `VoxelForgeCore` C++20 target and make both builds compile or link that target from the same
|
||||||
VoxelForge Unreal module. The plugin keeps ownership of asset loading, worker admission,
|
source files. For Unreal, first prototype the Unreal Build Tool integration: either a small core
|
||||||
cancellation, collision, game-thread mesh application, and per-frame upload budgets; workers call
|
module that compiles the library sources or a platform-built static library with explicit build
|
||||||
the shared density and meshing API with immutable contexts. The standalone keeps its D3D11 renderer
|
rules. Prefer source integration if packaging a binary per Unreal platform would add maintenance.
|
||||||
and JSON loader and calls the same API.
|
The plugin then replaces its copied evaluator and mesher calls with the core API behind a thin
|
||||||
|
adapter. Preserve plugin-owned UObject resolution, task cancellation, collision, game-thread mesh
|
||||||
|
application, and upload budgets. The standalone keeps its JSON loader and D3D11 renderer.
|
||||||
|
|
||||||
## Preventing drift before extraction
|
## Preventing drift before extraction
|
||||||
|
|
||||||
Until the move lands, keep the standalone copies synchronized from the plugin source with a small
|
Until the common target exists, keep a manifest of mirrored files and a sync/check script that names
|
||||||
manifest of shared files and a checked sync script. The script should compare hashes and refuse an
|
the canonical source for each file, compares hashes, and refuses when both copies have changed.
|
||||||
ambiguous two-sided edit. Run the exported 3,436-sample bitwise parity corpus after every density
|
Avoid hand-editing only one copy without recording that the edit is pending synchronization. After
|
||||||
change, then remove the duplicate source only when both hosts build against the shared library.
|
each density or meshing change, run the 3,436-sample bitwise corpus and the selected-tile density
|
||||||
|
hash check. Keep `/fp:precise`; do not accept evaluator changes that alter density bits as a speed
|
||||||
|
shortcut. A standalone-only optimization must be marked as pending synchronization until it is
|
||||||
|
ported or explicitly rejected for the canonical core. Delete the duplicate standalone source only
|
||||||
|
after both host builds use the common target and the parity corpus passes through each adapter.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "OwnerWorldLoader.h"
|
#include "OwnerWorldLoader.h"
|
||||||
#include "StreamingWorld.h"
|
#include "StreamingWorld.h"
|
||||||
#include "StandaloneResearchTunnels.h"
|
#include "StandaloneResearchTunnels.h"
|
||||||
|
#include "VoxelDensityProfile.h"
|
||||||
|
|
||||||
#ifdef TEXT
|
#ifdef TEXT
|
||||||
#undef TEXT
|
#undef TEXT
|
||||||
@@ -18,6 +19,8 @@
|
|||||||
#include <windowsx.h>
|
#include <windowsx.h>
|
||||||
|
|
||||||
#include <bit>
|
#include <bit>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
@@ -105,9 +108,16 @@ namespace
|
|||||||
};
|
};
|
||||||
struct FCrossingWork
|
struct FCrossingWork
|
||||||
{
|
{
|
||||||
|
uint64 SubmittedJobs=0;
|
||||||
|
uint64 PendingJobs=0;
|
||||||
|
uint64 FinishedJobs=0;
|
||||||
|
uint64 CancelledQueuedJobs=0;
|
||||||
|
uint64 StaleResults=0;
|
||||||
uint64 Tiles=0;
|
uint64 Tiles=0;
|
||||||
uint64 Samples=0;
|
uint64 Samples=0;
|
||||||
double WorkerSeconds=0.0;
|
double WorkerSeconds=0.0;
|
||||||
|
std::chrono::steady_clock::time_point StartTime{};
|
||||||
|
bool bReported=false;
|
||||||
};
|
};
|
||||||
|
|
||||||
FStandaloneBatchKey BatchForTile(const FVoxelTileKey& Tile)
|
FStandaloneBatchKey BatchForTile(const FVoxelTileKey& Tile)
|
||||||
@@ -133,6 +143,9 @@ namespace
|
|||||||
std::unique_ptr<FStandaloneOwnerWorld> World;
|
std::unique_ptr<FStandaloneOwnerWorld> World;
|
||||||
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
|
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
|
||||||
std::string DataPath;
|
std::string DataPath;
|
||||||
|
bool bHasCameraOverride=false;
|
||||||
|
V3 CameraPositionOverride{};
|
||||||
|
float CameraYawOverride=0.0f,CameraPitchOverride=0.0f;
|
||||||
std::unordered_map<FVoxelTileKey,FResidentTileMesh,FStandaloneTileKeyHash> ResidentTiles;
|
std::unordered_map<FVoxelTileKey,FResidentTileMesh,FStandaloneTileKeyHash> ResidentTiles;
|
||||||
std::unordered_map<FStandaloneBatchKey,std::vector<FVoxelTileKey>,FStandaloneBatchKeyHash> BatchMembers;
|
std::unordered_map<FStandaloneBatchKey,std::vector<FVoxelTileKey>,FStandaloneBatchKeyHash> BatchMembers;
|
||||||
std::unordered_map<FStandaloneBatchKey,FStreamingGpuBatch,FStandaloneBatchKeyHash> StreamingBatches;
|
std::unordered_map<FStandaloneBatchKey,FStreamingGpuBatch,FStandaloneBatchKeyHash> StreamingBatches;
|
||||||
@@ -143,6 +156,7 @@ namespace
|
|||||||
uint64 GeneratedByLevel[MaxLevels+1]{};
|
uint64 GeneratedByLevel[MaxLevels+1]{};
|
||||||
uint64 SamplesByLevel[MaxLevels+1]{};
|
uint64 SamplesByLevel[MaxLevels+1]{};
|
||||||
double WorkerSecondsByLevel[MaxLevels+1]{};
|
double WorkerSecondsByLevel[MaxLevels+1]{};
|
||||||
|
uint64 ProfileCyclesByLevel[MaxLevels+1][VoxelDensityProfile::BucketCount]{};
|
||||||
std::vector<double> TileSecondsByLevel[MaxLevels+1];
|
std::vector<double> TileSecondsByLevel[MaxLevels+1];
|
||||||
uint64 CrossingIndex=0;
|
uint64 CrossingIndex=0;
|
||||||
bool bGenerationRunning=false;
|
bool bGenerationRunning=false;
|
||||||
@@ -154,11 +168,23 @@ namespace
|
|||||||
bool bAutoCaptureWritten=false;
|
bool bAutoCaptureWritten=false;
|
||||||
bool bFullDetailReported=false;
|
bool bFullDetailReported=false;
|
||||||
bool bInitialClipmapReported=false;
|
bool bInitialClipmapReported=false;
|
||||||
|
bool bProfileAttribution=false;
|
||||||
|
bool bCompareEvaluators=false;
|
||||||
|
bool bParityOnly=false;
|
||||||
|
bool bDensityHash=false;
|
||||||
|
bool bTestOneChunkCrossing=false;
|
||||||
|
bool bCrossingMoveStarted=false;
|
||||||
|
bool bCrossingMoveFinished=false;
|
||||||
|
bool bCrossingClosePosted=false;
|
||||||
|
std::chrono::steady_clock::time_point CrossingMoveStopTime{};
|
||||||
|
bool bCaptureAfterClipmap=false;
|
||||||
|
bool bQuitAfterClipmap=false;
|
||||||
std::string CapturePath;
|
std::string CapturePath;
|
||||||
std::chrono::steady_clock::time_point GenerationStart{};
|
std::chrono::steady_clock::time_point GenerationStart{};
|
||||||
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
|
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
|
||||||
bool Keys[256]{};
|
bool Keys[256]{};
|
||||||
int32 Seed=0;
|
int32 Seed=0;
|
||||||
|
int32 WorkerCount=6;
|
||||||
int32 Width=1280,Height=800;
|
int32 Width=1280,Height=800;
|
||||||
float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f;
|
float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f;
|
||||||
V3 Position{};
|
V3 Position{};
|
||||||
@@ -190,7 +216,9 @@ namespace
|
|||||||
StreamingBatches.clear();BatchMembers.clear();ResidentTiles.clear();
|
StreamingBatches.clear();BatchMembers.clear();ResidentTiles.clear();
|
||||||
DirtyBatchSet.clear();DesiredTiles.clear();CrossingWork.clear();
|
DirtyBatchSet.clear();DesiredTiles.clear();CrossingWork.clear();
|
||||||
for(int32 I=0;I<=MaxLevels;++I)
|
for(int32 I=0;I<=MaxLevels;++I)
|
||||||
{DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0;TileSecondsByLevel[I].clear();}
|
{DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0;
|
||||||
|
std::fill(std::begin(ProfileCyclesByLevel[I]),std::end(ProfileCyclesByLevel[I]),0);
|
||||||
|
TileSecondsByLevel[I].clear();}
|
||||||
TotalVertices=0;TotalIndices=0;
|
TotalVertices=0;TotalIndices=0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,9 +363,12 @@ O VS(I i){O o;o.p=mul(float4(i.p,1),VP);o.world=i.p;o.n=normalize(i.n);return o;
|
|||||||
float4 PS(O i):SV_TARGET{
|
float4 PS(O i):SV_TARGET{
|
||||||
float3 N=normalize(i.n);float3 V=normalize(Camera-i.world);float3 L=normalize(-LightDir);
|
float3 N=normalize(i.n);float3 V=normalize(Camera-i.world);float3 L=normalize(-LightDir);
|
||||||
float Diffuse=saturate(dot(N,L));float Head=saturate(dot(N,V));
|
float Diffuse=saturate(dot(N,L));float Head=saturate(dot(N,V));
|
||||||
float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0);Head=Head*Head*Atten;
|
float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0);
|
||||||
float3 Rock=lerp(float3(0.28,0.34,0.38),float3(0.43,0.47,0.48),saturate(N.z*0.5+0.5));
|
float3 Rock=lerp(float3(0.30,0.22,0.16),float3(0.48,0.43,0.34),saturate(N.z*0.5+0.5));
|
||||||
float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.62,0.76,0.9)*Head*1.25;
|
// Keep the camera fill broad and low-amplitude. The old squared, high-intensity
|
||||||
|
// lobe made the 25 cm density-lattice normal variation read as horizontal bands.
|
||||||
|
Head=Head*Atten;
|
||||||
|
float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||||
float Fog=saturate((Dist-FogStart)/(FogEnd-FogStart));
|
float Fog=saturate((Dist-FogStart)/(FogEnd-FogStart));
|
||||||
Lit=lerp(Lit,float3(0.10,0.14,0.19),Fog);return float4(Lit,1);
|
Lit=lerp(Lit,float3(0.10,0.14,0.19),Fog);return float4(Lit,1);
|
||||||
})";
|
})";
|
||||||
@@ -361,9 +392,9 @@ float4 PS(O i):SV_TARGET{
|
|||||||
return Resize(Width,Height);
|
return Resize(Width,Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintParity()
|
size_t PrintParity()
|
||||||
{
|
{
|
||||||
if(bParityChecked||!World)return;
|
if(bParityChecked||!World)return 0;
|
||||||
bParityChecked=true;size_t MismatchCount=0;
|
bParityChecked=true;size_t MismatchCount=0;
|
||||||
std::map<std::string,size_t> Kinds;
|
std::map<std::string,size_t> Kinds;
|
||||||
for(size_t I=0;I<World->DensitySamples.size();++I)
|
for(size_t I=0;I<World->DensitySamples.size();++I)
|
||||||
@@ -384,6 +415,7 @@ float4 PS(O i):SV_TARGET{
|
|||||||
for(const auto& [Kind,Count]:Kinds)std::printf(" %-24s %zu\n",Kind.c_str(),Count);
|
for(const auto& [Kind,Count]:Kinds)std::printf(" %-24s %zu\n",Kind.c_str(),Count);
|
||||||
std::fflush(stdout);
|
std::fflush(stdout);
|
||||||
std::fflush(stdout);
|
std::fflush(stdout);
|
||||||
|
return MismatchCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConfigureWorld(std::string& Error)
|
bool ConfigureWorld(std::string& Error)
|
||||||
@@ -428,12 +460,22 @@ float4 PS(O i):SV_TARGET{
|
|||||||
BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels);
|
BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels);
|
||||||
GenerationStart=std::chrono::steady_clock::now();
|
GenerationStart=std::chrono::steady_clock::now();
|
||||||
bFullDetailReported=false;bInitialClipmapReported=false;bAutoCaptureWritten=false;
|
bFullDetailReported=false;bInitialClipmapReported=false;bAutoCaptureWritten=false;
|
||||||
|
if(bProfileAttribution)
|
||||||
|
{
|
||||||
|
VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Attribution,128);
|
||||||
|
VoxelDensityProfile::CalibrateTimer();
|
||||||
|
VoxelDensityProfile::Reset();
|
||||||
|
std::printf("Density stage profiling enabled (Attribution, sample interval 128).\n");
|
||||||
|
}
|
||||||
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
|
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
|
||||||
DesiredTiles=Desired.Tiles;
|
DesiredTiles=Desired.Tiles;
|
||||||
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
|
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
|
||||||
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
|
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
|
||||||
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,6);
|
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount);
|
||||||
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
|
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
|
||||||
|
FCrossingWork& InitialWork=CrossingWork[CrossingIndex];
|
||||||
|
InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs;
|
||||||
|
InitialWork.StartTime=GenerationStart;
|
||||||
bGenerationRunning=true;bRebuildPending=false;
|
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",
|
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,
|
Seed,bRoundTunnels?" with research round/winding tunnels":"",Update.DesiredTiles,
|
||||||
@@ -495,6 +537,15 @@ float4 PS(O i):SV_TARGET{
|
|||||||
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
||||||
++CrossingIndex;
|
++CrossingIndex;
|
||||||
const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(CameraVoxels,ResidentKeys,CrossingIndex);
|
const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(CameraVoxels,ResidentKeys,CrossingIndex);
|
||||||
|
for(const auto& Pair:Update.CancelledByCrossing)
|
||||||
|
{
|
||||||
|
FCrossingWork& Work=CrossingWork[Pair.first];
|
||||||
|
Work.PendingJobs=Work.PendingJobs>=Pair.second?Work.PendingJobs-Pair.second:0;
|
||||||
|
Work.CancelledQueuedJobs+=Pair.second;
|
||||||
|
}
|
||||||
|
FCrossingWork& Work=CrossingWork[CrossingIndex];
|
||||||
|
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
|
||||||
|
Work.StartTime=std::chrono::steady_clock::now();
|
||||||
BuildTargetChunk=Update.CenterChunk;bGenerationRunning=true;
|
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",
|
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,
|
static_cast<unsigned long long>(CrossingIndex),Update.CenterChunk.X,Update.CenterChunk.Y,Update.CenterChunk.Z,
|
||||||
@@ -502,6 +553,7 @@ float4 PS(O i):SV_TARGET{
|
|||||||
Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs,Leavers.size(),
|
Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs,Leavers.size(),
|
||||||
Update.CancelledQueuedJobs,Update.FullResTiles);
|
Update.CancelledQueuedJobs,Update.FullResTiles);
|
||||||
std::fflush(stdout);
|
std::fflush(stdout);
|
||||||
|
ReportCrossingWork();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RebuildBatch(const FStandaloneBatchKey& Key)
|
bool RebuildBatch(const FStandaloneBatchKey& Key)
|
||||||
@@ -621,10 +673,44 @@ float4 PS(O i):SV_TARGET{
|
|||||||
std::printf("[TileTiming] LOD=%d generated=%llu target=%d samples=%.2fM worker_sum=%.3fs avg=%.3fms p50=%.3fms p95=%.3fms\n",
|
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],
|
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);
|
static_cast<double>(SamplesByLevel[L])/1.0e6,WorkerSecondsByLevel[L],Average*1000.0,P50*1000.0,P95*1000.0);
|
||||||
|
if(bProfileAttribution)
|
||||||
|
{
|
||||||
|
std::printf("[TileProfile] LOD=%d",L);
|
||||||
|
for(size_t I=0;I<VoxelDensityProfile::BucketCount;++I)
|
||||||
|
{
|
||||||
|
const double Ms=static_cast<double>(ProfileCyclesByLevel[L][I])
|
||||||
|
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
|
||||||
|
if(Ms>0.001)std::printf(" %s=%.3fms",
|
||||||
|
VoxelDensityProfile::BucketName(static_cast<VoxelDensityProfile::EBucket>(I)),Ms);
|
||||||
|
}
|
||||||
|
std::printf("\n");
|
||||||
|
}
|
||||||
Times.clear();
|
Times.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReportCrossingWork()
|
||||||
|
{
|
||||||
|
for(auto& Pair:CrossingWork)
|
||||||
|
{
|
||||||
|
FCrossingWork& Work=Pair.second;
|
||||||
|
if(Work.bReported||Work.PendingJobs!=0)continue;
|
||||||
|
Work.bReported=true;
|
||||||
|
const double AverageMs=Work.FinishedJobs>0
|
||||||
|
?Work.WorkerSeconds*1000.0/static_cast<double>(Work.FinishedJobs):0.0;
|
||||||
|
const double WallSeconds=Work.StartTime.time_since_epoch().count()!=0
|
||||||
|
?std::chrono::duration<double>(std::chrono::steady_clock::now()-Work.StartTime).count():0.0;
|
||||||
|
std::printf("[ChunkCrossingWork] id=%llu submitted=%llu finished=%llu generated=%llu cancelled_queued=%llu stale=%llu samples=%.2fM worker_sum=%.3fs wall=%.3fs avg=%.3fms\n",
|
||||||
|
static_cast<unsigned long long>(Pair.first),
|
||||||
|
static_cast<unsigned long long>(Work.SubmittedJobs),
|
||||||
|
static_cast<unsigned long long>(Work.FinishedJobs),
|
||||||
|
static_cast<unsigned long long>(Work.Tiles),
|
||||||
|
static_cast<unsigned long long>(Work.CancelledQueuedJobs),
|
||||||
|
static_cast<unsigned long long>(Work.StaleResults),
|
||||||
|
static_cast<double>(Work.Samples)/1.0e6,Work.WorkerSeconds,WallSeconds,AverageMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PumpGeneration()
|
void PumpGeneration()
|
||||||
{
|
{
|
||||||
if(bRebuildPending){StartGeneration();return;}
|
if(bRebuildPending){StartGeneration();return;}
|
||||||
@@ -632,11 +718,16 @@ float4 PS(O i):SV_TARGET{
|
|||||||
std::vector<FStandaloneCpuTile> Ready=Streamer->TakeReady();
|
std::vector<FStandaloneCpuTile> Ready=Streamer->TakeReady();
|
||||||
for(FStandaloneCpuTile& Tile:Ready)
|
for(FStandaloneCpuTile& Tile:Ready)
|
||||||
{
|
{
|
||||||
if(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key))continue;
|
|
||||||
const int32 Level=Tile.Key.Level;
|
const int32 Level=Tile.Key.Level;
|
||||||
FCrossingWork& Crossing=CrossingWork[Tile.RequestedCrossing];
|
FCrossingWork& Crossing=CrossingWork[Tile.RequestedCrossing];
|
||||||
++Crossing.Tiles;Crossing.Samples+=Tile.DensitySamples;Crossing.WorkerSeconds+=Tile.WorkerSeconds;
|
if(Crossing.PendingJobs>0)--Crossing.PendingJobs;
|
||||||
|
++Crossing.FinishedJobs;Crossing.Samples+=Tile.DensitySamples;Crossing.WorkerSeconds+=Tile.WorkerSeconds;
|
||||||
|
if(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key))
|
||||||
|
{++Crossing.StaleResults;continue;}
|
||||||
|
++Crossing.Tiles;
|
||||||
++GeneratedByLevel[Level];SamplesByLevel[Level]+=Tile.DensitySamples;WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
|
++GeneratedByLevel[Level];SamplesByLevel[Level]+=Tile.DensitySamples;WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
|
||||||
|
if(bProfileAttribution)for(size_t I=0;I<Tile.ProfileCycles.size();++I)
|
||||||
|
ProfileCyclesByLevel[Level][I]+=Tile.ProfileCycles[I];
|
||||||
TileSecondsByLevel[Level].push_back(Tile.WorkerSeconds);
|
TileSecondsByLevel[Level].push_back(Tile.WorkerSeconds);
|
||||||
FResidentTileMesh Resident;Resident.Vertices=std::move(Tile.Vertices);Resident.Indices=std::move(Tile.Indices);
|
FResidentTileMesh Resident;Resident.Vertices=std::move(Tile.Vertices);Resident.Indices=std::move(Tile.Indices);
|
||||||
Resident.Samples=Tile.DensitySamples;Resident.WorkerSeconds=Tile.WorkerSeconds;
|
Resident.Samples=Tile.DensitySamples;Resident.WorkerSeconds=Tile.WorkerSeconds;
|
||||||
@@ -644,7 +735,20 @@ float4 PS(O i):SV_TARGET{
|
|||||||
const FStandaloneBatchKey Batch=BatchForTile(Tile.Key);
|
const FStandaloneBatchKey Batch=BatchForTile(Tile.Key);
|
||||||
BatchMembers[Batch].push_back(Tile.Key);MarkBatchDirty(Batch);
|
BatchMembers[Batch].push_back(Tile.Key);MarkBatchDirty(Batch);
|
||||||
}
|
}
|
||||||
|
ReportCrossingWork();
|
||||||
UploadDirtyBatches();
|
UploadDirtyBatches();
|
||||||
|
if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted
|
||||||
|
&&DirtyBatchSet.empty())
|
||||||
|
{
|
||||||
|
const auto Crossing=CrossingWork.find(1);
|
||||||
|
if(Crossing!=CrossingWork.end()&&Crossing->second.bReported)
|
||||||
|
{
|
||||||
|
bCrossingClosePosted=true;
|
||||||
|
std::printf("[CrossingTestComplete] crossing work and its bounded uploads drained; closing this test viewer.\n");
|
||||||
|
std::fflush(stdout);
|
||||||
|
PostMessageA(Window,WM_CLOSE,0,0);
|
||||||
|
}
|
||||||
|
}
|
||||||
if(!bFullDetailReported&&IsFullDetailUploaded())
|
if(!bFullDetailReported&&IsFullDetailUploaded())
|
||||||
{
|
{
|
||||||
bFullDetailReported=true;
|
bFullDetailReported=true;
|
||||||
@@ -661,13 +765,15 @@ float4 PS(O i):SV_TARGET{
|
|||||||
Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices),
|
Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices),
|
||||||
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
|
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
|
||||||
ReportTileTimings();std::fflush(stdout);
|
ReportTileTimings();std::fflush(stdout);
|
||||||
|
if(bQuitAfterClipmap&&CapturePath.empty())PostMessageA(Window,WM_CLOSE,0,0);
|
||||||
}
|
}
|
||||||
bGenerationRunning=ResidentTiles.size()!=DesiredTiles.size()||!DirtyBatchSet.empty();
|
bGenerationRunning=ResidentTiles.size()!=DesiredTiles.size()||!DirtyBatchSet.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CaptureRenderedFrame()
|
void CaptureRenderedFrame()
|
||||||
{
|
{
|
||||||
if(CapturePath.empty()||bAutoCaptureWritten||!IsFullDetailUploaded())return;
|
const bool bReadyToCapture=bCaptureAfterClipmap?bInitialClipmapReported:IsFullDetailUploaded();
|
||||||
|
if(CapturePath.empty()||bAutoCaptureWritten||!bReadyToCapture)return;
|
||||||
bAutoCaptureWritten=SaveBackBufferPng(CapturePath);
|
bAutoCaptureWritten=SaveBackBufferPng(CapturePath);
|
||||||
if(bAutoCaptureWritten)
|
if(bAutoCaptureWritten)
|
||||||
{
|
{
|
||||||
@@ -698,6 +804,21 @@ float4 PS(O i):SV_TARGET{
|
|||||||
{
|
{
|
||||||
PumpGeneration();
|
PumpGeneration();
|
||||||
const auto Now=std::chrono::steady_clock::now();
|
const auto Now=std::chrono::steady_clock::now();
|
||||||
|
if(bTestOneChunkCrossing&&bFullDetailReported&&!bCrossingMoveStarted&&!bCrossingMoveFinished)
|
||||||
|
{
|
||||||
|
bCrossingMoveStarted=true;
|
||||||
|
CrossingMoveStopTime=Now+std::chrono::milliseconds(350);
|
||||||
|
Keys[VK_SHIFT]=true;Keys['W']=true;
|
||||||
|
std::printf("[CrossingTestMove] moving forward for 350 ms after full detail.\n");
|
||||||
|
std::fflush(stdout);
|
||||||
|
}
|
||||||
|
if(bCrossingMoveStarted&&Now>=CrossingMoveStopTime)
|
||||||
|
{
|
||||||
|
Keys[VK_SHIFT]=false;Keys['W']=false;
|
||||||
|
bCrossingMoveStarted=false;bCrossingMoveFinished=true;
|
||||||
|
std::printf("[CrossingTestMove] movement stopped; waiting for crossing work and uploads.\n");
|
||||||
|
std::fflush(stdout);
|
||||||
|
}
|
||||||
const float Dt=std::clamp(std::chrono::duration<float>(Now-PreviousFrame).count(),0.0f,0.1f);
|
const float Dt=std::clamp(std::chrono::duration<float>(Now-PreviousFrame).count(),0.0f,0.1f);
|
||||||
PreviousFrame=Now;FrameMs=Dt*1000.0;
|
PreviousFrame=Now;FrameMs=Dt*1000.0;
|
||||||
const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F));
|
const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F));
|
||||||
@@ -767,23 +888,241 @@ float4 PS(O i):SV_TARGET{
|
|||||||
const std::filesystem::path Exe(Module);
|
const std::filesystem::path Exe(Module);
|
||||||
return Exe.parent_path().parent_path()/L"data"/L"owner_world.json";
|
return Exe.parent_path().parent_path()/L"data"/L"owner_world.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int RunTileBenchmark(FStandaloneOwnerWorld& World,V3 CameraPosition,int32 Level,
|
||||||
|
int32 Count,bool bProfile,bool bCompareEvaluators,bool bDensityHash)
|
||||||
|
{
|
||||||
|
const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0);
|
||||||
|
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraVoxels);
|
||||||
|
std::vector<FVoxelTileKey> Tiles;Tiles.reserve(static_cast<size_t>(std::max(Count,1)));
|
||||||
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
||||||
|
if(Tile.Level==Level&&static_cast<int32>(Tiles.size())<Count)Tiles.push_back(Tile);
|
||||||
|
if(Tiles.empty())
|
||||||
|
{std::fprintf(stderr,"No LOD %d tiles are in the current desired set.\n",Level);return 4;}
|
||||||
|
|
||||||
|
VoxelGenLOD::InitializeConsoleSwitches();
|
||||||
|
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
||||||
|
int32 TileCacheWindow=1,TileCacheWindowMinLOD=3,RoomGraphCacheSlots=1;
|
||||||
|
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindow=",TileCacheWindow);
|
||||||
|
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindowMinLOD=",TileCacheWindowMinLOD);
|
||||||
|
FParse::Value(FCommandLine::Get(),"voxel.RoomGraphCacheSlots=",RoomGraphCacheSlots);
|
||||||
|
std::printf("[BenchmarkConfig] tile_cache_window=%d fused_min_lod=%d room_graph_cache_slots=%d command='%s'\n",
|
||||||
|
TileCacheWindow,TileCacheWindowMinLOD,RoomGraphCacheSlots,FCommandLine::Get());
|
||||||
|
VoxelDensityProfile::SetMode(bProfile?VoxelDensityProfile::EMode::Attribution
|
||||||
|
:VoxelDensityProfile::EMode::Disabled,128);
|
||||||
|
if(bProfile)VoxelDensityProfile::CalibrateTimer();
|
||||||
|
VoxelDensityProfile::Reset();
|
||||||
|
const VoxelDensityProfile::FSnapshot Before=VoxelDensityProfile::SnapshotCurrentThread();
|
||||||
|
|
||||||
|
UVoxelMarchingCubesMesher Mesher;
|
||||||
|
Mesher.SetGenerator(World.Generator.get());Mesher.bGenerateSkirts=World.bGenerateSkirts;
|
||||||
|
Mesher.SkirtCells=static_cast<float>(World.SkirtCells);Mesher.LODOctaveDrop=World.LODOctaveDrop;
|
||||||
|
const int32 BandMin=World.StrateContentCutMinLevel<9&&Desired.bHasStrate
|
||||||
|
?Desired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
|
||||||
|
const int32 BandMax=World.StrateContentCutMinLevel<9&&Desired.bHasStrate
|
||||||
|
?(Desired.StrateTopChunkZ+1)*CHUNK_SIZE:MAX_int32;
|
||||||
|
if(bCompareEvaluators)
|
||||||
|
{
|
||||||
|
const bool bOriginalFused=VoxelGenLOD::IsUseFusedEvaluatorEnabled();
|
||||||
|
uint64 Mismatches=0,ComparedSamples=0,MeshCountMismatches=0;
|
||||||
|
float MaxAbsoluteDifference=0.0f;
|
||||||
|
size_t FirstMismatch=SIZE_MAX;
|
||||||
|
FVoxelTileKey FirstMismatchTile{};
|
||||||
|
VoxelGenLOD::SetUseFusedEvaluatorEnabled(true);
|
||||||
|
for(const FVoxelTileKey& Tile:Tiles)
|
||||||
|
{
|
||||||
|
const int32 Cells=Level<World.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World.CoarseTileCells);
|
||||||
|
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
|
||||||
|
std::vector<float> FusedDensities,OpStackDensities;
|
||||||
|
int64 FusedSampleCalls=0,OpStackSampleCalls=0;
|
||||||
|
FVoxelMeshData FusedMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
||||||
|
BandMin,BandMax,&FusedSampleCalls,nullptr,nullptr,&FusedDensities);
|
||||||
|
VoxelGenLOD::SetUseFusedEvaluatorEnabled(false);
|
||||||
|
FVoxelMeshData OpStackMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
||||||
|
BandMin,BandMax,&OpStackSampleCalls,nullptr,nullptr,&OpStackDensities);
|
||||||
|
VoxelGenLOD::SetUseFusedEvaluatorEnabled(true);
|
||||||
|
if(FusedMesh.Vertices.Num()!=OpStackMesh.Vertices.Num()
|
||||||
|
|| FusedMesh.Triangles.Num()!=OpStackMesh.Triangles.Num())++MeshCountMismatches;
|
||||||
|
const size_t SharedSamples=std::min(FusedDensities.size(),OpStackDensities.size());
|
||||||
|
ComparedSamples+=SharedSamples;
|
||||||
|
for(size_t I=0;I<SharedSamples;++I)
|
||||||
|
{
|
||||||
|
if(std::bit_cast<uint32>(FusedDensities[I])!=std::bit_cast<uint32>(OpStackDensities[I]))
|
||||||
|
{
|
||||||
|
if(FirstMismatch==SIZE_MAX){FirstMismatch=I;FirstMismatchTile=Tile;}
|
||||||
|
++Mismatches;
|
||||||
|
MaxAbsoluteDifference=std::max(MaxAbsoluteDifference,
|
||||||
|
std::abs(FusedDensities[I]-OpStackDensities[I]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mismatches+=static_cast<uint64>(std::max(FusedDensities.size(),OpStackDensities.size())-SharedSamples);
|
||||||
|
}
|
||||||
|
std::printf("[EvaluatorCompare] LOD=%d tiles=%zu compared_samples=%llu density_mismatches=%llu first_tile=(%d,%d,%d) first_index=%llu max_abs=%.9g mesh_count_mismatch_tiles=%llu original_fused=%d\n",
|
||||||
|
Level,Tiles.size(),static_cast<unsigned long long>(ComparedSamples),
|
||||||
|
static_cast<unsigned long long>(Mismatches),
|
||||||
|
FirstMismatchTile.Coord.X,FirstMismatchTile.Coord.Y,FirstMismatchTile.Coord.Z,
|
||||||
|
static_cast<unsigned long long>(FirstMismatch),MaxAbsoluteDifference,
|
||||||
|
static_cast<unsigned long long>(MeshCountMismatches),bOriginalFused?1:0);
|
||||||
|
VoxelGenLOD::SetUseFusedEvaluatorEnabled(bOriginalFused);
|
||||||
|
std::fflush(stdout);
|
||||||
|
return Mismatches==0?0:5;
|
||||||
|
}
|
||||||
|
std::vector<double> TileSeconds;TileSeconds.reserve(Tiles.size());
|
||||||
|
uint64 Samples=0,Vertices=0,Indices=0;
|
||||||
|
uint64 DensityHash=14695981039346656037ull;
|
||||||
|
const auto TotalStart=std::chrono::steady_clock::now();
|
||||||
|
for(const FVoxelTileKey& Tile:Tiles)
|
||||||
|
{
|
||||||
|
const auto TileStart=std::chrono::steady_clock::now();
|
||||||
|
const int32 Cells=Level<World.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World.CoarseTileCells);
|
||||||
|
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
|
||||||
|
int64 TileSamples=0;
|
||||||
|
std::vector<float> DebugDensitySamples;
|
||||||
|
FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
||||||
|
BandMin,BandMax,&TileSamples,nullptr,nullptr,
|
||||||
|
bDensityHash?&DebugDensitySamples:nullptr);
|
||||||
|
Samples+=static_cast<uint64>(FMath::Max<int64>(0,TileSamples));
|
||||||
|
Vertices+=static_cast<uint64>(Mesh.Vertices.Num());
|
||||||
|
Indices+=static_cast<uint64>(Mesh.Triangles.Num());
|
||||||
|
if(bDensityHash)
|
||||||
|
{
|
||||||
|
for(const float Density:DebugDensitySamples)
|
||||||
|
{
|
||||||
|
DensityHash^=std::bit_cast<uint32>(Density);
|
||||||
|
DensityHash*=1099511628211ull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TileSeconds.push_back(std::chrono::duration<double>(
|
||||||
|
std::chrono::steady_clock::now()-TileStart).count());
|
||||||
|
}
|
||||||
|
const double TotalSeconds=std::chrono::duration<double>(
|
||||||
|
std::chrono::steady_clock::now()-TotalStart).count();
|
||||||
|
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
|
||||||
|
std::sort(TileSeconds.begin(),TileSeconds.end());
|
||||||
|
const double Average=TotalSeconds/static_cast<double>(Tiles.size());
|
||||||
|
std::printf("[TileBenchmark] LOD=%d generated=%zu samples=%.2fM worker_sum=%.3fs avg=%.3fms p50=%.3fms p95=%.3fms vertices=%llu indices=%llu profiling=%s\n",
|
||||||
|
Level,Tiles.size(),static_cast<double>(Samples)/1.0e6,TotalSeconds,Average*1000.0,
|
||||||
|
TileSeconds[(TileSeconds.size()-1)*50/100]*1000.0,
|
||||||
|
TileSeconds[(TileSeconds.size()-1)*95/100]*1000.0,
|
||||||
|
static_cast<unsigned long long>(Vertices),static_cast<unsigned long long>(Indices),
|
||||||
|
bProfile?"Attribution":"off");
|
||||||
|
if(bDensityHash)
|
||||||
|
{
|
||||||
|
std::printf("[TileDensityHash] LOD=%d tiles=%zu hash=0x%016llx\n",Level,Tiles.size(),
|
||||||
|
static_cast<unsigned long long>(DensityHash));
|
||||||
|
}
|
||||||
|
if(bProfile)
|
||||||
|
{
|
||||||
|
std::printf("[TileProfile] LOD=%d",Level);
|
||||||
|
for(int32 I=0;I<VoxelDensityProfile::BucketCount;++I)
|
||||||
|
{
|
||||||
|
const uint64 CycleDelta=After.Cycles[I]>=Before.Cycles[I]
|
||||||
|
?After.Cycles[I]-Before.Cycles[I]:0;
|
||||||
|
const double Ms=static_cast<double>(CycleDelta)
|
||||||
|
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
|
||||||
|
if(Ms>0.001)std::printf(" %s=%.3fms",VoxelDensityProfile::BucketName(
|
||||||
|
static_cast<VoxelDensityProfile::EBucket>(I)),Ms);
|
||||||
|
}
|
||||||
|
std::printf("\n");
|
||||||
|
const auto CounterDelta=[&](VoxelDensityProfile::ECounter Counter)
|
||||||
|
{
|
||||||
|
const size_t I=static_cast<size_t>(Counter);
|
||||||
|
return After.Counters[I]>=Before.Counters[I]
|
||||||
|
?After.Counters[I]-Before.Counters[I]:0;
|
||||||
|
};
|
||||||
|
std::printf("[TileCounters] sdf_builds=%llu graph_tile_builds=%llu graph_core_builds=%llu op_block_builds=%llu\n",
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfCacheBuild)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTile)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTunnelCore)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockBuilds)));
|
||||||
|
std::printf("[SdfRebuildCauses] strate=%llu seed=%llu fingerprint=%llu layout=%llu manager=%llu terrain_ops=%llu fused_policy=%llu tile_policy=%llu tile_identity=%llu bounds_x=%llu bounds_y=%llu tile_window_accepted=%llu tile_window_rejected=%llu\n",
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildStrate)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildSeed)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFingerprint)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildLayout)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTerrainOps)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFusedPolicy)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTilePolicy)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTileIdentity)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsX)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsY)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowAccepted)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowRejected)));
|
||||||
|
std::printf("[TilePolicyKeyChanges] origin=%llu step=%llu cells=%llu fingerprint=%llu layout=%llu manager=%llu terrain_ops=%llu fused_path=%llu context=%llu\n",
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyOrigin)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyStep)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyCells)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFingerprint)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyLayout)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyManager)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyTerrainOps)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFusedPath)),
|
||||||
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyContext)));
|
||||||
|
}
|
||||||
|
std::fflush(stdout);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int Argc,char** Argv)
|
int main(int Argc,char** Argv)
|
||||||
{
|
{
|
||||||
FApp App;App.DataPath=DefaultDataPath().string();
|
FApp App;App.DataPath=DefaultDataPath().string();
|
||||||
|
std::string JoinedCommandLine;
|
||||||
|
for(int32 I=1;I<Argc;++I)
|
||||||
|
{if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];}
|
||||||
|
FCommandLine::Set(JoinedCommandLine);
|
||||||
|
int32 BenchmarkLevel=-1,BenchmarkCount=64;
|
||||||
for(int32 I=1;I<Argc;++I)
|
for(int32 I=1;I<Argc;++I)
|
||||||
{
|
{
|
||||||
const std::string_view Arg=Argv[I];
|
const std::string_view Arg=Argv[I];
|
||||||
if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10));
|
if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10));
|
||||||
|
else if(Arg=="--profile-attribution")App.bProfileAttribution=true;
|
||||||
|
else if(Arg=="--compare-evaluators")App.bCompareEvaluators=true;
|
||||||
|
else if(Arg=="--parity-only")App.bParityOnly=true;
|
||||||
|
else if(Arg=="--density-hash")App.bDensityHash=true;
|
||||||
|
else if(Arg=="--round-tunnels")App.bRoundTunnels=true;
|
||||||
|
else if(Arg=="--test-one-chunk-crossing")App.bTestOneChunkCrossing=true;
|
||||||
|
else if(Arg=="--capture-after-clipmap")App.bCaptureAfterClipmap=true;
|
||||||
|
else if(Arg=="--quit-after-clipmap")App.bQuitAfterClipmap=true;
|
||||||
|
else if(Arg.starts_with("--workers="))
|
||||||
|
{
|
||||||
|
const int32 Parsed=std::atoi(std::string(Arg.substr(10)).c_str());
|
||||||
|
App.WorkerCount=std::clamp(Parsed,1,32);
|
||||||
|
}
|
||||||
|
else if(Arg.starts_with("--benchmark-level="))
|
||||||
|
BenchmarkLevel=std::atoi(std::string(Arg.substr(18)).c_str());
|
||||||
|
else if(Arg.starts_with("--benchmark-count="))
|
||||||
|
BenchmarkCount=std::max(1,std::atoi(std::string(Arg.substr(18)).c_str()));
|
||||||
|
else if(Arg.starts_with("--camera="))
|
||||||
|
{
|
||||||
|
const std::string Values(Arg.substr(9));
|
||||||
|
float X=0,Y=0,Z=0,YawDegrees=0,PitchDegrees=0;
|
||||||
|
if(std::sscanf(Values.c_str(),"%f,%f,%f,%f,%f",&X,&Y,&Z,&YawDegrees,&PitchDegrees)==5)
|
||||||
|
{
|
||||||
|
App.bHasCameraOverride=true;App.CameraPositionOverride={X,Y,Z};
|
||||||
|
App.CameraYawOverride=YawDegrees*0.017453292519943295f;
|
||||||
|
App.CameraPitchOverride=PitchDegrees*0.017453292519943295f;
|
||||||
|
}
|
||||||
|
else {std::fprintf(stderr,"Invalid --camera value: %s\n",Values.c_str());return 2;}
|
||||||
|
}
|
||||||
else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg);
|
else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg);
|
||||||
}
|
}
|
||||||
std::string Error;
|
std::string Error;
|
||||||
App.World=std::make_unique<FStandaloneOwnerWorld>();
|
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;}
|
if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}
|
||||||
App.Seed=App.World->Seed;
|
App.Seed=App.World->Seed;
|
||||||
|
if(App.bParityOnly)
|
||||||
|
{
|
||||||
|
VoxelGenLOD::InitializeConsoleSwitches();
|
||||||
|
return App.PrintParity()==0?0:1;
|
||||||
|
}
|
||||||
App.Position={static_cast<float>(App.World->ViewerStartVoxels.X*0.25),
|
App.Position={static_cast<float>(App.World->ViewerStartVoxels.X*0.25),
|
||||||
static_cast<float>(App.World->ViewerStartVoxels.Y*0.25),static_cast<float>(App.World->ViewerStartVoxels.Z*0.25)};
|
static_cast<float>(App.World->ViewerStartVoxels.Y*0.25),static_cast<float>(App.World->ViewerStartVoxels.Z*0.25)};
|
||||||
|
if(App.bHasCameraOverride){App.Position=App.CameraPositionOverride;App.Yaw=App.CameraYawOverride;App.Pitch=App.CameraPitchOverride;}
|
||||||
|
if(BenchmarkLevel>=0)
|
||||||
|
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
|
||||||
|
App.bProfileAttribution,App.bCompareEvaluators,App.bDensityHash);
|
||||||
App.World.reset();
|
App.World.reset();
|
||||||
|
|
||||||
WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld";
|
WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld";
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "WorldMesher.h"
|
#include "WorldMesher.h"
|
||||||
|
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <array>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
struct FStandaloneTileKeyHash
|
struct FStandaloneTileKeyHash
|
||||||
@@ -30,6 +31,7 @@ struct FStandaloneCpuTile
|
|||||||
std::vector<uint32> Indices;
|
std::vector<uint32> Indices;
|
||||||
uint64 DensitySamples=0;
|
uint64 DensitySamples=0;
|
||||||
double WorkerSeconds=0.0;
|
double WorkerSeconds=0.0;
|
||||||
|
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FStandaloneStreamingUpdate
|
struct FStandaloneStreamingUpdate
|
||||||
@@ -40,6 +42,7 @@ struct FStandaloneStreamingUpdate
|
|||||||
int32 NewJobs=0;
|
int32 NewJobs=0;
|
||||||
int32 CancelledQueuedJobs=0;
|
int32 CancelledQueuedJobs=0;
|
||||||
int32 FullResTiles=0;
|
int32 FullResTiles=0;
|
||||||
|
std::vector<std::pair<uint64,uint64>> CancelledByCrossing;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FStandaloneTileJob
|
struct FStandaloneTileJob
|
||||||
@@ -48,6 +51,20 @@ struct FStandaloneTileJob
|
|||||||
int32 BandZMin=MIN_int32;
|
int32 BandZMin=MIN_int32;
|
||||||
int32 BandZMax=MAX_int32;
|
int32 BandZMax=MAX_int32;
|
||||||
uint64 RequestedCrossing=0;
|
uint64 RequestedCrossing=0;
|
||||||
|
double PriorityDistanceSquared=0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FStandaloneTileJobHeapOrder
|
||||||
|
{
|
||||||
|
bool operator()(const FStandaloneTileJob& A,const FStandaloneTileJob& B)const
|
||||||
|
{
|
||||||
|
if(A.PriorityDistanceSquared!=B.PriorityDistanceSquared)
|
||||||
|
return A.PriorityDistanceSquared>B.PriorityDistanceSquared;
|
||||||
|
if(A.Key.Level!=B.Key.Level)return A.Key.Level>B.Key.Level;
|
||||||
|
if(A.Key.Coord.Z!=B.Key.Coord.Z)return A.Key.Coord.Z>B.Key.Coord.Z;
|
||||||
|
if(A.Key.Coord.Y!=B.Key.Coord.Y)return A.Key.Coord.Y>B.Key.Coord.Y;
|
||||||
|
return A.Key.Coord.X>B.Key.Coord.X;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace StandaloneWorldMesher
|
namespace StandaloneWorldMesher
|
||||||
@@ -99,6 +116,7 @@ public:
|
|||||||
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=6)
|
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=6)
|
||||||
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,32))
|
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,32))
|
||||||
{
|
{
|
||||||
|
VoxelGenLOD::InitializeConsoleSwitches();
|
||||||
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
||||||
}
|
}
|
||||||
FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete;
|
FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete;
|
||||||
@@ -153,12 +171,20 @@ public:
|
|||||||
Desired.clear();Desired.reserve(NewDesired.Tiles.size()*2+1);
|
Desired.clear();Desired.reserve(NewDesired.Tiles.size()*2+1);
|
||||||
for(const FVoxelTileKey& Key:NewDesired.Tiles)Desired.insert(Key);
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)Desired.insert(Key);
|
||||||
FullResTileCount=Stats.FullResTiles;
|
FullResTileCount=Stats.FullResTiles;
|
||||||
|
std::unordered_map<uint64,uint64> CancelledByCrossing;
|
||||||
auto QueueEnd=std::remove_if(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& Job)
|
auto QueueEnd=std::remove_if(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& Job)
|
||||||
{
|
{
|
||||||
if(Desired.contains(Job.Key))return false;
|
if(Desired.contains(Job.Key))return false;
|
||||||
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;return true;
|
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;
|
||||||
|
++CancelledByCrossing[Job.RequestedCrossing];return true;
|
||||||
});
|
});
|
||||||
Queue.erase(QueueEnd,Queue.end());
|
Queue.erase(QueueEnd,Queue.end());
|
||||||
|
Stats.CancelledByCrossing.reserve(CancelledByCrossing.size());
|
||||||
|
for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair);
|
||||||
|
|
||||||
|
for(FStandaloneTileJob& Job:Queue)
|
||||||
|
Job.PriorityDistanceSquared=StandaloneWorldMesher::TileDistanceSquaredChunks(
|
||||||
|
Job.Key,NewDesired.CenterChunk);
|
||||||
|
|
||||||
const int32 BandMin=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate
|
const int32 BandMin=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate
|
||||||
? NewDesired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
|
? NewDesired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
|
||||||
@@ -167,16 +193,12 @@ public:
|
|||||||
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
||||||
{
|
{
|
||||||
if(Resident.contains(Key)||Outstanding.contains(Key))continue;
|
if(Resident.contains(Key)||Outstanding.contains(Key))continue;
|
||||||
Queue.push_back({Key,BandMin,BandMax,CrossingIndex});
|
Queue.push_back({Key,BandMin,BandMax,CrossingIndex,
|
||||||
|
StandaloneWorldMesher::TileDistanceSquaredChunks(Key,NewDesired.CenterChunk)});
|
||||||
Outstanding.insert(Key);++Stats.NewJobs;
|
Outstanding.insert(Key);++Stats.NewJobs;
|
||||||
}
|
}
|
||||||
CurrentDesired=std::move(NewDesired);
|
CurrentDesired=std::move(NewDesired);
|
||||||
std::sort(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& A,const FStandaloneTileJob& B)
|
std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
|
||||||
{
|
|
||||||
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();
|
Wake.notify_all();
|
||||||
return Stats;
|
return Stats;
|
||||||
}
|
}
|
||||||
@@ -215,8 +237,21 @@ private:
|
|||||||
const int32 Cells=Level<World->FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells);
|
const int32 Cells=Level<World->FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells);
|
||||||
const int32 Step=FMath::Max(1,Extent/Cells);
|
const int32 Step=FMath::Max(1,Extent/Cells);
|
||||||
int64 SampleCount=0;
|
int64 SampleCount=0;
|
||||||
|
const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled;
|
||||||
|
const VoxelDensityProfile::FSnapshot Before=bProfile
|
||||||
|
? VoxelDensityProfile::SnapshotCurrentThread():VoxelDensityProfile::FSnapshot{};
|
||||||
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
|
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
|
||||||
Job.BandZMin,Job.BandZMax,&SampleCount);
|
Job.BandZMin,Job.BandZMax,&SampleCount);
|
||||||
|
if(bProfile)
|
||||||
|
{
|
||||||
|
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
|
||||||
|
for(size_t I=0;I<Out.ProfileCycles.size();++I)
|
||||||
|
{
|
||||||
|
const size_t Bucket=I;
|
||||||
|
Out.ProfileCycles[I]=After.Cycles[Bucket]>=Before.Cycles[Bucket]
|
||||||
|
?After.Cycles[Bucket]-Before.Cycles[Bucket]:0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Out.DensitySamples=static_cast<uint64>(FMath::Max<int64>(0,SampleCount));
|
Out.DensitySamples=static_cast<uint64>(FMath::Max<int64>(0,SampleCount));
|
||||||
Out.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
|
Out.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
|
||||||
for(int32 I=0;I<Mesh.Vertices.Num();++I)
|
for(int32 I=0;I<Mesh.Vertices.Num();++I)
|
||||||
@@ -241,7 +276,8 @@ private:
|
|||||||
std::unique_lock<std::mutex> Lock(Mutex);
|
std::unique_lock<std::mutex> Lock(Mutex);
|
||||||
Wake.wait(Lock,[&](){return bStop||!Queue.empty();});
|
Wake.wait(Lock,[&](){return bStop||!Queue.empty();});
|
||||||
if(bStop)return;
|
if(bStop)return;
|
||||||
Job=Queue.front();Queue.erase(Queue.begin());
|
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
|
||||||
|
Job=Queue.back();Queue.pop_back();
|
||||||
}
|
}
|
||||||
FStandaloneCpuTile Tile=Generate(Job);
|
FStandaloneCpuTile Tile=Generate(Job);
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7750,7 +7750,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
GraphLowLink.Init(INDEX_NONE, NumRooms);
|
GraphLowLink.Init(INDEX_NONE, NumRooms);
|
||||||
GraphBridges.Init(0u, GraphEdges.Num());
|
GraphBridges.Init(0u, GraphEdges.Num());
|
||||||
int32 GraphDiscoveryTime = 0;
|
int32 GraphDiscoveryTime = 0;
|
||||||
TFunction<void(int32, int32)> VisitGraph = [&](int32 Node, int32 ParentEdge)
|
const auto VisitGraph = [&](auto&& VisitGraphSelf, int32 Node, int32 ParentEdge) -> void
|
||||||
{
|
{
|
||||||
GraphDiscovery[Node] = GraphDiscoveryTime;
|
GraphDiscovery[Node] = GraphDiscoveryTime;
|
||||||
GraphLowLink[Node] = GraphDiscoveryTime;
|
GraphLowLink[Node] = GraphDiscoveryTime;
|
||||||
@@ -7765,7 +7765,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
const int32 Other = Edge.RoomA == Node ? Edge.RoomB : Edge.RoomA;
|
const int32 Other = Edge.RoomA == Node ? Edge.RoomB : Edge.RoomA;
|
||||||
if (GraphDiscovery[Other] == INDEX_NONE)
|
if (GraphDiscovery[Other] == INDEX_NONE)
|
||||||
{
|
{
|
||||||
VisitGraph(Other, EdgeIndex);
|
VisitGraphSelf(VisitGraphSelf, Other, EdgeIndex);
|
||||||
GraphLowLink[Node] = FMath::Min(
|
GraphLowLink[Node] = FMath::Min(
|
||||||
GraphLowLink[Node], GraphLowLink[Other]);
|
GraphLowLink[Node], GraphLowLink[Other]);
|
||||||
if (GraphLowLink[Other] > GraphDiscovery[Node])
|
if (GraphLowLink[Other] > GraphDiscovery[Node])
|
||||||
@@ -7784,7 +7784,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
{
|
{
|
||||||
if (GraphDiscovery[Node] == INDEX_NONE)
|
if (GraphDiscovery[Node] == INDEX_NONE)
|
||||||
{
|
{
|
||||||
VisitGraph(Node, INDEX_NONE);
|
VisitGraph(VisitGraph, Node, INDEX_NONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex)
|
for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex)
|
||||||
@@ -7803,7 +7803,7 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
TArray<uint8, TInlineAllocator<128>> GraphForestEdges;
|
TArray<uint8, TInlineAllocator<128>> GraphForestEdges;
|
||||||
GraphForestVisited.Init(0u, NumRooms);
|
GraphForestVisited.Init(0u, NumRooms);
|
||||||
GraphForestEdges.Init(0u, GraphEdges.Num());
|
GraphForestEdges.Init(0u, GraphEdges.Num());
|
||||||
TFunction<void(int32)> VisitWalkForest = [&](int32 Node)
|
const auto VisitWalkForest = [&](auto&& VisitWalkForestSelf, int32 Node) -> void
|
||||||
{
|
{
|
||||||
GraphForestVisited[Node] = 1u;
|
GraphForestVisited[Node] = 1u;
|
||||||
for (const int32 EdgeIndex : GraphAdjacency[Node])
|
for (const int32 EdgeIndex : GraphAdjacency[Node])
|
||||||
@@ -7815,14 +7815,14 @@ void VoxelCaveMorphology::BuildChunkCache(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
GraphForestEdges[EdgeIndex] = 1u;
|
GraphForestEdges[EdgeIndex] = 1u;
|
||||||
VisitWalkForest(Other);
|
VisitWalkForestSelf(VisitWalkForestSelf, Other);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (int32 Node = 0; Node < NumRooms; ++Node)
|
for (int32 Node = 0; Node < NumRooms; ++Node)
|
||||||
{
|
{
|
||||||
if (GraphForestVisited[Node] == 0u)
|
if (GraphForestVisited[Node] == 0u)
|
||||||
{
|
{
|
||||||
VisitWalkForest(Node);
|
VisitWalkForest(VisitWalkForest, Node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex)
|
for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex)
|
||||||
|
|||||||
@@ -704,6 +704,28 @@ namespace VoxelDensityProfile
|
|||||||
case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes");
|
case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes");
|
||||||
case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes");
|
case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes");
|
||||||
case ECounter::OpBlockCopyBytes: return TEXT("OpBlockCopyBytes");
|
case ECounter::OpBlockCopyBytes: return TEXT("OpBlockCopyBytes");
|
||||||
|
case ECounter::SdfRebuildStrate: return TEXT("SdfRebuildStrate");
|
||||||
|
case ECounter::SdfRebuildSeed: return TEXT("SdfRebuildSeed");
|
||||||
|
case ECounter::SdfRebuildFingerprint: return TEXT("SdfRebuildFingerprint");
|
||||||
|
case ECounter::SdfRebuildLayout: return TEXT("SdfRebuildLayout");
|
||||||
|
case ECounter::SdfRebuildManagerLifetime: return TEXT("SdfRebuildManagerLifetime");
|
||||||
|
case ECounter::SdfRebuildTerrainOps: return TEXT("SdfRebuildTerrainOps");
|
||||||
|
case ECounter::SdfRebuildFusedPolicy: return TEXT("SdfRebuildFusedPolicy");
|
||||||
|
case ECounter::SdfRebuildTilePolicy: return TEXT("SdfRebuildTilePolicy");
|
||||||
|
case ECounter::SdfRebuildTileIdentity: return TEXT("SdfRebuildTileIdentity");
|
||||||
|
case ECounter::SdfRebuildBoundsX: return TEXT("SdfRebuildBoundsX");
|
||||||
|
case ECounter::SdfRebuildBoundsY: return TEXT("SdfRebuildBoundsY");
|
||||||
|
case ECounter::SdfTileWindowAccepted: return TEXT("SdfTileWindowAccepted");
|
||||||
|
case ECounter::SdfTileWindowRejected: return TEXT("SdfTileWindowRejected");
|
||||||
|
case ECounter::TilePolicyKeyOrigin: return TEXT("TilePolicyKeyOrigin");
|
||||||
|
case ECounter::TilePolicyKeyStep: return TEXT("TilePolicyKeyStep");
|
||||||
|
case ECounter::TilePolicyKeyCells: return TEXT("TilePolicyKeyCells");
|
||||||
|
case ECounter::TilePolicyKeyFingerprint: return TEXT("TilePolicyKeyFingerprint");
|
||||||
|
case ECounter::TilePolicyKeyLayout: return TEXT("TilePolicyKeyLayout");
|
||||||
|
case ECounter::TilePolicyKeyManager: return TEXT("TilePolicyKeyManager");
|
||||||
|
case ECounter::TilePolicyKeyTerrainOps: return TEXT("TilePolicyKeyTerrainOps");
|
||||||
|
case ECounter::TilePolicyKeyFusedPath: return TEXT("TilePolicyKeyFusedPath");
|
||||||
|
case ECounter::TilePolicyKeyContext: return TEXT("TilePolicyKeyContext");
|
||||||
case ECounter::Count: break;
|
case ECounter::Count: break;
|
||||||
}
|
}
|
||||||
return TEXT("Unknown");
|
return TEXT("Unknown");
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <array>
|
||||||
|
|
||||||
namespace VoxelMath
|
namespace VoxelMath
|
||||||
{
|
{
|
||||||
@@ -139,6 +140,24 @@ namespace
|
|||||||
TEXT("voxel.TileCacheWindowOpStackMinLOD"),
|
TEXT("voxel.TileCacheWindowOpStackMinLOD"),
|
||||||
GVoxelForgeTileCacheWindowOpStackMinLOD,
|
GVoxelForgeTileCacheWindowOpStackMinLOD,
|
||||||
TEXT("Minimum SampleStep LOD for the op-stack tile-sized room cache window."));
|
TEXT("Minimum SampleStep LOD for the op-stack tile-sized room cache window."));
|
||||||
|
int32 GVoxelForgeRoomGraphCacheSlots = 1;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeRoomGraphCacheSlots(
|
||||||
|
TEXT("voxel.RoomGraphCacheSlots"),
|
||||||
|
GVoxelForgeRoomGraphCacheSlots,
|
||||||
|
TEXT("Per-worker exact room-graph windows to retain during density sampling (1-4)."));
|
||||||
|
bool GVoxelForgeRoomGraphCacheSlotsParsed = false;
|
||||||
|
|
||||||
|
void VF_ParseRoomGraphCacheSlots()
|
||||||
|
{
|
||||||
|
if (GVoxelForgeRoomGraphCacheSlotsParsed) return;
|
||||||
|
GVoxelForgeRoomGraphCacheSlotsParsed = true;
|
||||||
|
int32 CommandLineValue = GVoxelForgeRoomGraphCacheSlots;
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.RoomGraphCacheSlots="), CommandLineValue))
|
||||||
|
{
|
||||||
|
GVoxelForgeRoomGraphCacheSlots = CommandLineValue;
|
||||||
|
}
|
||||||
|
GVoxelForgeRoomGraphCacheSlots = FMath::Clamp(GVoxelForgeRoomGraphCacheSlots, 1, 4);
|
||||||
|
}
|
||||||
|
|
||||||
int32 GVoxelForgeSpatialIndex = 1;
|
int32 GVoxelForgeSpatialIndex = 1;
|
||||||
FAutoConsoleVariableRef CVarVoxelForgeSpatialIndex(
|
FAutoConsoleVariableRef CVarVoxelForgeSpatialIndex(
|
||||||
@@ -879,10 +898,23 @@ void VoxelGenLOD::InitializeConsoleSwitches()
|
|||||||
VF_ParseFusedEvaluatorSwitch();
|
VF_ParseFusedEvaluatorSwitch();
|
||||||
VF_ParseWormBlockSkipSwitch();
|
VF_ParseWormBlockSkipSwitch();
|
||||||
VF_ParseTileCacheWindowSwitch();
|
VF_ParseTileCacheWindowSwitch();
|
||||||
|
VF_ParseRoomGraphCacheSlots();
|
||||||
VF_ParseSpatialIndexSwitch();
|
VF_ParseSpatialIndexSwitch();
|
||||||
VF_ParseChunkRoutingCacheSwitch();
|
VF_ParseChunkRoutingCacheSwitch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VoxelGenLOD::SetUseFusedEvaluatorEnabled(bool bEnabled)
|
||||||
|
{
|
||||||
|
VF_ParseFusedEvaluatorSwitch();
|
||||||
|
GVoxelForgeUseFusedEvaluator = bEnabled ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VoxelGenLOD::IsUseFusedEvaluatorEnabled()
|
||||||
|
{
|
||||||
|
VF_ParseFusedEvaluatorSwitch();
|
||||||
|
return GVoxelForgeUseFusedEvaluator != 0;
|
||||||
|
}
|
||||||
|
|
||||||
bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath)
|
bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath)
|
||||||
{
|
{
|
||||||
if (GVoxelForgeTileCacheWindow == 0)
|
if (GVoxelForgeTileCacheWindow == 0)
|
||||||
@@ -4171,38 +4203,31 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
//
|
//
|
||||||
// Declared before the RoomDensity if-block so SDFCache and NearestRoomIdx
|
// Declared before the RoomDensity if-block so SDFCache and NearestRoomIdx
|
||||||
// are also visible to the terrain ops block further below.
|
// are also visible to the terrain ops block further below.
|
||||||
thread_local FChunkSDFCache SDFCache;
|
// Keep a small bounded set of exact room-graph windows. Tall 32-chunk tiles need two slots
|
||||||
// Cache validity is tracked by the SEARCH BOX the cache was built for, NOT by chunk
|
// for their two strata; other tile extents start with one. Every slot uses the full cache
|
||||||
// equality. Gradient-normal sampling queries at WorldX±1 (and warp displacement) can
|
// identity/bounds.
|
||||||
// step a voxel outside the chunk; with chunk-equality keying that flipped the key and
|
struct FRoomGraphCacheSlot
|
||||||
// rebuilt the (now expensive) cache every boundary cell. Since the stored rooms cover
|
{
|
||||||
// the search box + MaxInfluence, any query INSIDE the box is correct — so we only
|
FChunkSDFCache Cache;
|
||||||
// rebuild when the query actually leaves the box. Result: one build per chunk, no thrash.
|
float SMinX = 1.0f, SMaxX = -1.0f;
|
||||||
thread_local float CachedSMinX = 1.0f, CachedSMaxX = -1.0f; // start invalid (min > max)
|
float SMinY = 0.0f, SMaxY = 0.0f;
|
||||||
thread_local float CachedSMinY = 0.0f, CachedSMaxY = 0.0f;
|
int32 Strate = INT32_MIN;
|
||||||
thread_local int32 CachedStrate = INT32_MIN;
|
uint32 Seed = 0;
|
||||||
thread_local uint32 CachedSeed = 0;
|
uint32 Fingerprint = 0xFFFFFFFFu;
|
||||||
// ⚠️ AUDIT §C2 — `CachedFingerprint` et `CachedLayout` ci-dessous sont OBLIGATOIRES dans cette clé.
|
uint32 Layout = 0xFFFFFFFFu;
|
||||||
// La clé ci-dessus décrit la GÉOMÉTRIE de la fenêtre (boîte, strate, seed) et rien de ce qui
|
bool bUsesFusedCacheWindow = false;
|
||||||
// détermine les PARAMS avec lesquels les salles ont été cuites. Comme `GetGenerationParams`
|
bool bUsesTileCacheWindow = false;
|
||||||
// blende à l'intérieur d'une strate (`Alpha` = f(chunk Z), et f(chunk XY) aussi en
|
FIntVector TileOrigin = FIntVector::ZeroValue;
|
||||||
// `Interleaved`), deux chunks voisins produisent la MÊME clé avec des params DIFFÉRENTS, et le
|
int32 TileStep = 0;
|
||||||
// deuxième se sert des salles du premier. Non déterministe entre pairs, parce que l'ordre des
|
int32 TileCells = 0;
|
||||||
// workers décide lequel est « le premier » — exactement ce que §2.6.1 interdit.
|
uint64 ManagerLifetimeId = 0;
|
||||||
//
|
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr;
|
||||||
// Pourquoi ça ne casse PAS l'invariant de perf de §8.10 : la clé reste une BOÎTE, donc les
|
uint64 LastUse = 0;
|
||||||
// sondes de gradient à `WorldX ± 1` ne font toujours pas tourner le cache. Ce qui le fait
|
bool bValid = false;
|
||||||
// tourner en plus, c'est un changement RÉEL de params — une fois par chunk dans une bande de
|
};
|
||||||
// transition, ce qui est exactement le nombre de reconstructions nécessaire.
|
static thread_local std::array<FRoomGraphCacheSlot, 4> RoomGraphCacheSlots{};
|
||||||
thread_local uint32 CachedFingerprint = 0xFFFFFFFFu;
|
static thread_local uint64 RoomGraphCacheUseSerial = 0;
|
||||||
thread_local uint32 CachedLayout = 0xFFFFFFFFu;
|
VF_ParseRoomGraphCacheSlots();
|
||||||
thread_local bool CachedUsesFusedCacheWindow = false;
|
|
||||||
thread_local bool CachedUsesTileCacheWindow = false;
|
|
||||||
thread_local FIntVector CachedTileOrigin = FIntVector::ZeroValue;
|
|
||||||
thread_local int32 CachedTileStep = 0;
|
|
||||||
thread_local int32 CachedTileCells = 0;
|
|
||||||
thread_local uint64 CachedManagerLifetimeId = 0;
|
|
||||||
thread_local const TArray<FStrateTerrainOpEntry>* CachedTerrainOps = nullptr;
|
|
||||||
// The envelope proof is constant for a cache-policy key. Do not run the complete room/edge
|
// The envelope proof is constant for a cache-policy key. Do not run the complete room/edge
|
||||||
// reach calculation for every density sample: that would turn a build-time guard into a
|
// reach calculation for every density sample: that would turn a build-time guard into a
|
||||||
// per-sample cost on the LOD0 floor.
|
// per-sample cost on the LOD0 floor.
|
||||||
@@ -4267,6 +4292,32 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
|| CachedTilePolicyTerrainOps != TerrainOps;
|
|| CachedTilePolicyTerrainOps != TerrainOps;
|
||||||
if (bTilePolicyKeyChanged)
|
if (bTilePolicyKeyChanged)
|
||||||
{
|
{
|
||||||
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
|
{
|
||||||
|
const auto CountPolicyKeyChange = [](bool bCondition, VoxelDensityProfile::ECounter Counter)
|
||||||
|
{
|
||||||
|
if (bCondition) VoxelDensityProfile::AddCounter(Counter);
|
||||||
|
};
|
||||||
|
CountPolicyKeyChange(CachedTilePolicyOrigin != RequestedTileOrigin,
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyOrigin);
|
||||||
|
CountPolicyKeyChange(CachedTilePolicySampleStep != RequestedTileStep
|
||||||
|
|| CachedTilePolicyStep != RequestedTileStep,
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyStep);
|
||||||
|
CountPolicyKeyChange(CachedTilePolicyCells != RequestedTileCells,
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyCells);
|
||||||
|
CountPolicyKeyChange(CachedTilePolicyFingerprint != ParamsFingerprint,
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyFingerprint);
|
||||||
|
CountPolicyKeyChange(CachedTilePolicyLayout != LayoutVersion,
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyLayout);
|
||||||
|
CountPolicyKeyChange(CachedTilePolicyManagerLifetimeId != CurrentManagerLifetimeId,
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyManager);
|
||||||
|
CountPolicyKeyChange(CachedTilePolicyTerrainOps != TerrainOps,
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyTerrainOps);
|
||||||
|
CountPolicyKeyChange(CachedTilePolicyFusedPath != bUseFusedCacheWindow,
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyFusedPath);
|
||||||
|
CountPolicyKeyChange(CachedTilePolicyHasContext != (bTileWindowEligible && bHasTileWindowContext),
|
||||||
|
VoxelDensityProfile::ECounter::TilePolicyKeyContext);
|
||||||
|
}
|
||||||
CachedTilePolicyValid = true;
|
CachedTilePolicyValid = true;
|
||||||
CachedTilePolicyHasContext = bTileWindowEligible && bHasTileWindowContext;
|
CachedTilePolicyHasContext = bTileWindowEligible && bHasTileWindowContext;
|
||||||
CachedTilePolicyFusedPath = bUseFusedCacheWindow;
|
CachedTilePolicyFusedPath = bUseFusedCacheWindow;
|
||||||
@@ -4280,6 +4331,12 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
CachedTilePolicyTerrainOps = TerrainOps;
|
CachedTilePolicyTerrainOps = TerrainOps;
|
||||||
CachedTilePolicyResult = CachedTilePolicyHasContext
|
CachedTilePolicyResult = CachedTilePolicyHasContext
|
||||||
&& VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps);
|
&& VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps);
|
||||||
|
if (VoxelDensityProfile::AreCountersEnabled() && CachedTilePolicyHasContext)
|
||||||
|
{
|
||||||
|
VoxelDensityProfile::AddCounter(CachedTilePolicyResult
|
||||||
|
? VoxelDensityProfile::ECounter::SdfTileWindowAccepted
|
||||||
|
: VoxelDensityProfile::ECounter::SdfTileWindowRejected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const bool bUseTileCacheWindow = CachedTilePolicyResult;
|
const bool bUseTileCacheWindow = CachedTilePolicyResult;
|
||||||
|
|
||||||
@@ -4290,36 +4347,137 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
|
|
||||||
float CaveSDF = FLT_MAX;
|
float CaveSDF = FLT_MAX;
|
||||||
|
|
||||||
|
// Get strate index for unique caves per strate. The index changes only at chunk-Z
|
||||||
|
// boundaries and is also part of the worker-local graph-cache identity.
|
||||||
|
int32 StrateIdx = 0;
|
||||||
|
if (StrateManager)
|
||||||
|
{
|
||||||
|
thread_local int32 SI_ChunkZ = INT32_MAX;
|
||||||
|
thread_local uint32 SI_Version = 0xFFFFFFFFu;
|
||||||
|
thread_local int32 SI_Index = 0;
|
||||||
|
const int32 QZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
|
||||||
|
const uint32 LV = StrateManager->GetLayoutVersion();
|
||||||
|
if (QZ != SI_ChunkZ || LV != SI_Version)
|
||||||
|
{
|
||||||
|
SI_ChunkZ = QZ;
|
||||||
|
SI_Version = LV;
|
||||||
|
SI_Index = StrateManager->GetStrateIndex(
|
||||||
|
((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE);
|
||||||
|
}
|
||||||
|
StrateIdx = SI_Index;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float ChunkMinX = CacheRegionX * static_cast<float>(CacheRegionSize);
|
||||||
|
const float ChunkMinY = CacheRegionY * static_cast<float>(CacheRegionSize);
|
||||||
|
const float ChunkMaxX = ChunkMinX + static_cast<float>(CacheRegionSize);
|
||||||
|
const float ChunkMaxY = ChunkMinY + static_cast<float>(CacheRegionSize);
|
||||||
|
const float WarpMargin = FMath::Abs(Params.CaveWarpStrength)
|
||||||
|
* VOXEL_NOISE_SCALE * 1.5f;
|
||||||
|
float DesiredSMinX = ChunkMinX;
|
||||||
|
float DesiredSMinY = ChunkMinY;
|
||||||
|
float DesiredSMaxX = ChunkMaxX;
|
||||||
|
float DesiredSMaxY = ChunkMaxY;
|
||||||
|
if (bUseTileCacheWindow)
|
||||||
|
{
|
||||||
|
const float TileMinX = static_cast<float>(RequestedTileOrigin.X);
|
||||||
|
const float TileMinY = static_cast<float>(RequestedTileOrigin.Y);
|
||||||
|
const float TileExtent = static_cast<float>(
|
||||||
|
static_cast<int64>(RequestedTileStep)
|
||||||
|
* static_cast<int64>(RequestedTileCells));
|
||||||
|
const float TileMaxX = TileMinX + TileExtent;
|
||||||
|
const float TileMaxY = TileMinY + TileExtent;
|
||||||
|
const float TileHalo = static_cast<float>(RequestedTileStep) + 2.0f;
|
||||||
|
const float Expansion = WarpMargin + TileHalo;
|
||||||
|
DesiredSMinX = TileMinX - Expansion;
|
||||||
|
DesiredSMinY = TileMinY - Expansion;
|
||||||
|
DesiredSMaxX = TileMaxX + Expansion;
|
||||||
|
DesiredSMaxY = TileMaxY + Expansion;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const float Expansion = WarpMargin + 2.0f;
|
||||||
|
DesiredSMinX = ChunkMinX - Expansion;
|
||||||
|
DesiredSMinY = ChunkMinY - Expansion;
|
||||||
|
DesiredSMaxX = ChunkMaxX + Expansion;
|
||||||
|
DesiredSMaxY = ChunkMaxY + Expansion;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int64 RequestedTileExtent = static_cast<int64>(RequestedTileStep)
|
||||||
|
* static_cast<int64>(RequestedTileCells);
|
||||||
|
const bool bNeedsTwoStrataWindows = bHasTileWindowContext
|
||||||
|
&& RequestedTileExtent == static_cast<int64>(CHUNK_SIZE) * 32;
|
||||||
|
const int32 ActiveCacheSlots = FMath::Max(
|
||||||
|
FMath::Clamp(GVoxelForgeRoomGraphCacheSlots, 1, 4),
|
||||||
|
bNeedsTwoStrataWindows ? 2 : 1);
|
||||||
|
FRoomGraphCacheSlot* CacheSlot = nullptr;
|
||||||
|
FRoomGraphCacheSlot* EvictionSlot = &RoomGraphCacheSlots[0];
|
||||||
|
for (int32 SlotIndex = 0; SlotIndex < ActiveCacheSlots; ++SlotIndex)
|
||||||
|
{
|
||||||
|
FRoomGraphCacheSlot& Candidate = RoomGraphCacheSlots[SlotIndex];
|
||||||
|
if (!Candidate.bValid)
|
||||||
|
{
|
||||||
|
EvictionSlot = &Candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (Candidate.LastUse < EvictionSlot->LastUse)
|
||||||
|
{
|
||||||
|
EvictionSlot = &Candidate;
|
||||||
|
}
|
||||||
|
const bool bSameKey = Candidate.Strate == StrateIdx
|
||||||
|
&& Candidate.Seed == static_cast<uint32>(Seed)
|
||||||
|
&& Candidate.Fingerprint == ParamsFingerprint
|
||||||
|
&& Candidate.Layout == LayoutVersion
|
||||||
|
&& Candidate.ManagerLifetimeId == CurrentManagerLifetimeId
|
||||||
|
&& Candidate.TerrainOps == TerrainOps
|
||||||
|
&& Candidate.bUsesFusedCacheWindow == bUseFusedCacheWindow
|
||||||
|
&& Candidate.bUsesTileCacheWindow == bUseTileCacheWindow
|
||||||
|
&& (!bUseTileCacheWindow
|
||||||
|
|| (Candidate.TileOrigin == RequestedTileOrigin
|
||||||
|
&& Candidate.TileStep == RequestedTileStep
|
||||||
|
&& Candidate.TileCells == RequestedTileCells));
|
||||||
|
const bool bSameBounds = Candidate.SMinX == DesiredSMinX
|
||||||
|
&& Candidate.SMaxX == DesiredSMaxX
|
||||||
|
&& Candidate.SMinY == DesiredSMinY
|
||||||
|
&& Candidate.SMaxY == DesiredSMaxY;
|
||||||
|
const bool bContainsQuery = WarpedX >= Candidate.SMinX
|
||||||
|
&& WarpedX <= Candidate.SMaxX
|
||||||
|
&& WarpedY >= Candidate.SMinY
|
||||||
|
&& WarpedY <= Candidate.SMaxY;
|
||||||
|
if (bSameKey && bSameBounds && bContainsQuery)
|
||||||
|
{
|
||||||
|
CacheSlot = &Candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CacheSlot == nullptr)
|
||||||
|
{
|
||||||
|
CacheSlot = EvictionSlot;
|
||||||
|
}
|
||||||
|
CacheSlot->LastUse = ++RoomGraphCacheUseSerial;
|
||||||
|
FChunkSDFCache& SDFCache = CacheSlot->Cache;
|
||||||
|
float& CachedSMinX = CacheSlot->SMinX;
|
||||||
|
float& CachedSMaxX = CacheSlot->SMaxX;
|
||||||
|
float& CachedSMinY = CacheSlot->SMinY;
|
||||||
|
float& CachedSMaxY = CacheSlot->SMaxY;
|
||||||
|
int32& CachedStrate = CacheSlot->Strate;
|
||||||
|
uint32& CachedSeed = CacheSlot->Seed;
|
||||||
|
uint32& CachedFingerprint = CacheSlot->Fingerprint;
|
||||||
|
uint32& CachedLayout = CacheSlot->Layout;
|
||||||
|
bool& CachedUsesFusedCacheWindow = CacheSlot->bUsesFusedCacheWindow;
|
||||||
|
bool& CachedUsesTileCacheWindow = CacheSlot->bUsesTileCacheWindow;
|
||||||
|
FIntVector& CachedTileOrigin = CacheSlot->TileOrigin;
|
||||||
|
int32& CachedTileStep = CacheSlot->TileStep;
|
||||||
|
int32& CachedTileCells = CacheSlot->TileCells;
|
||||||
|
uint64& CachedManagerLifetimeId = CacheSlot->ManagerLifetimeId;
|
||||||
|
const TArray<FStrateTerrainOpEntry>*& CachedTerrainOps = CacheSlot->TerrainOps;
|
||||||
|
|
||||||
if (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
if (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
|
||||||
{
|
{
|
||||||
// Get strate index for unique caves per strate.
|
|
||||||
// MEMOISED: the index only changes at chunk-Z boundaries, yet this ran the strate layout's
|
|
||||||
// linear scan PER VOXEL. Key = (chunk-Z, layout version) — the version guards against editor
|
|
||||||
// rebuilds (RebuildStrates / live edit) serving a stale index. The lookup queries the BAND
|
|
||||||
// CENTRE so the result is a pure function of the key (order-independent, window-invariant).
|
|
||||||
int32 StrateIdx = 0;
|
|
||||||
if (StrateManager)
|
|
||||||
{
|
|
||||||
thread_local int32 SI_ChunkZ = INT32_MAX;
|
|
||||||
thread_local uint32 SI_Version = 0xFFFFFFFFu;
|
|
||||||
thread_local int32 SI_Index = 0;
|
|
||||||
const int32 QZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
|
|
||||||
const uint32 LV = StrateManager->GetLayoutVersion();
|
|
||||||
if (QZ != SI_ChunkZ || LV != SI_Version)
|
|
||||||
{
|
|
||||||
SI_ChunkZ = QZ;
|
|
||||||
SI_Version = LV;
|
|
||||||
// GetStrateIndex expects Unreal world units (divides by VOXEL_SIZE internally);
|
|
||||||
// WorldZ here is in voxels, so multiply by VOXEL_SIZE.
|
|
||||||
SI_Index = StrateManager->GetStrateIndex(((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE);
|
|
||||||
}
|
|
||||||
StrateIdx = SI_Index;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild only when the WARPED query (what EvaluateSDFCached uses) leaves the
|
// Rebuild only when the WARPED query (what EvaluateSDFCached uses) leaves the
|
||||||
// cached search box, or the strate/seed changed.
|
// cached search box, or the strate/seed changed.
|
||||||
const bool bNeedRebuild =
|
const bool bNeedRebuild =
|
||||||
StrateIdx != CachedStrate || (uint32)Seed != CachedSeed ||
|
!CacheSlot->bValid || StrateIdx != CachedStrate || (uint32)Seed != CachedSeed ||
|
||||||
ParamsFingerprint != CachedFingerprint || LayoutVersion != CachedLayout ||
|
ParamsFingerprint != CachedFingerprint || LayoutVersion != CachedLayout ||
|
||||||
CurrentManagerLifetimeId != CachedManagerLifetimeId ||
|
CurrentManagerLifetimeId != CachedManagerLifetimeId ||
|
||||||
TerrainOps != CachedTerrainOps ||
|
TerrainOps != CachedTerrainOps ||
|
||||||
@@ -4334,58 +4492,44 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
|
|
||||||
if (bNeedRebuild)
|
if (bNeedRebuild)
|
||||||
{
|
{
|
||||||
// The lowered evaluator uses the same window-invariance contract as the prepared
|
if (VoxelDensityProfile::AreCountersEnabled())
|
||||||
// room-graph source. With the switch enabled, the search box is the exact requesting
|
|
||||||
// tile footprint plus the one-sample normal halo and deterministic warp margin. A
|
|
||||||
// tile worker can visit neighbouring chunk keys in an arbitrary order without
|
|
||||||
// rebuilding the graph at every coarse sample. Point queries without a tile context
|
|
||||||
// retain the historical fused four-chunk / ordinary one-chunk fallback.
|
|
||||||
const float ChunkMinX = CacheRegionX * (float)CacheRegionSize;
|
|
||||||
const float ChunkMinY = CacheRegionY * (float)CacheRegionSize;
|
|
||||||
const float ChunkMaxX = ChunkMinX + (float)CacheRegionSize;
|
|
||||||
const float ChunkMaxY = ChunkMinY + (float)CacheRegionSize;
|
|
||||||
const float WarpMargin = FMath::Abs(Params.CaveWarpStrength)
|
|
||||||
* VOXEL_NOISE_SCALE * 1.5f;
|
|
||||||
float SMinX = ChunkMinX;
|
|
||||||
float SMinY = ChunkMinY;
|
|
||||||
float SMaxX = ChunkMaxX;
|
|
||||||
float SMaxY = ChunkMaxY;
|
|
||||||
if (bUseTileCacheWindow)
|
|
||||||
{
|
{
|
||||||
const float TileMinX = static_cast<float>(RequestedTileOrigin.X);
|
const auto CountIf = [](bool bCondition, VoxelDensityProfile::ECounter Counter)
|
||||||
const float TileMinY = static_cast<float>(RequestedTileOrigin.Y);
|
{
|
||||||
const float TileExtent = static_cast<float>(
|
if (bCondition) VoxelDensityProfile::AddCounter(Counter);
|
||||||
static_cast<int64>(RequestedTileStep)
|
};
|
||||||
* static_cast<int64>(RequestedTileCells));
|
CountIf(StrateIdx != CachedStrate, VoxelDensityProfile::ECounter::SdfRebuildStrate);
|
||||||
const float TileMaxX = TileMinX + TileExtent;
|
CountIf(static_cast<uint32>(Seed) != CachedSeed, VoxelDensityProfile::ECounter::SdfRebuildSeed);
|
||||||
const float TileMaxY = TileMinY + TileExtent;
|
CountIf(ParamsFingerprint != CachedFingerprint, VoxelDensityProfile::ECounter::SdfRebuildFingerprint);
|
||||||
const float TileHalo = static_cast<float>(RequestedTileStep) + 2.0f;
|
CountIf(LayoutVersion != CachedLayout, VoxelDensityProfile::ECounter::SdfRebuildLayout);
|
||||||
const float Expansion = WarpMargin + TileHalo;
|
CountIf(CurrentManagerLifetimeId != CachedManagerLifetimeId,
|
||||||
SMinX = TileMinX - Expansion;
|
VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime);
|
||||||
SMinY = TileMinY - Expansion;
|
CountIf(TerrainOps != CachedTerrainOps, VoxelDensityProfile::ECounter::SdfRebuildTerrainOps);
|
||||||
SMaxX = TileMaxX + Expansion;
|
CountIf(bUseFusedCacheWindow != CachedUsesFusedCacheWindow,
|
||||||
SMaxY = TileMaxY + Expansion;
|
VoxelDensityProfile::ECounter::SdfRebuildFusedPolicy);
|
||||||
|
CountIf(bUseTileCacheWindow != CachedUsesTileCacheWindow,
|
||||||
|
VoxelDensityProfile::ECounter::SdfRebuildTilePolicy);
|
||||||
|
CountIf(bUseTileCacheWindow
|
||||||
|
&& (RequestedTileOrigin != CachedTileOrigin
|
||||||
|
|| RequestedTileStep != CachedTileStep
|
||||||
|
|| RequestedTileCells != CachedTileCells),
|
||||||
|
VoxelDensityProfile::ECounter::SdfRebuildTileIdentity);
|
||||||
|
CountIf(WarpedX < CachedSMinX || WarpedX > CachedSMaxX,
|
||||||
|
VoxelDensityProfile::ECounter::SdfRebuildBoundsX);
|
||||||
|
CountIf(WarpedY < CachedSMinY || WarpedY > CachedSMaxY,
|
||||||
|
VoxelDensityProfile::ECounter::SdfRebuildBoundsY);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
const float Expansion = WarpMargin + 2.0f;
|
|
||||||
SMinX = ChunkMinX - Expansion;
|
|
||||||
SMinY = ChunkMinY - Expansion;
|
|
||||||
SMaxX = ChunkMaxX + Expansion;
|
|
||||||
SMaxY = ChunkMaxY + Expansion;
|
|
||||||
}
|
|
||||||
|
|
||||||
VoxelCaveMorphology::BuildChunkCache(
|
VoxelCaveMorphology::BuildChunkCache(
|
||||||
SDFCache,
|
SDFCache,
|
||||||
SMinX, SMinY, SMaxX, SMaxY,
|
DesiredSMinX, DesiredSMinY, DesiredSMaxX, DesiredSMaxY,
|
||||||
Params, (uint32)Seed, StrateIdx,
|
Params, (uint32)Seed, StrateIdx,
|
||||||
TerrainOps,
|
TerrainOps,
|
||||||
ERoomGraphBuildSite::GeneratorTile
|
ERoomGraphBuildSite::GeneratorTile
|
||||||
);
|
);
|
||||||
UpdateTunnelCoreTileReach(SDFCache, bUseTileCacheWindow);
|
UpdateTunnelCoreTileReach(SDFCache, bUseTileCacheWindow);
|
||||||
|
|
||||||
CachedSMinX = SMinX; CachedSMaxX = SMaxX;
|
CachedSMinX = DesiredSMinX; CachedSMaxX = DesiredSMaxX;
|
||||||
CachedSMinY = SMinY; CachedSMaxY = SMaxY;
|
CachedSMinY = DesiredSMinY; CachedSMaxY = DesiredSMaxY;
|
||||||
CachedStrate = StrateIdx;
|
CachedStrate = StrateIdx;
|
||||||
CachedSeed = (uint32)Seed;
|
CachedSeed = (uint32)Seed;
|
||||||
CachedFingerprint = ParamsFingerprint;
|
CachedFingerprint = ParamsFingerprint;
|
||||||
@@ -4398,6 +4542,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|
|||||||
CachedTileCells = bUseTileCacheWindow ? RequestedTileCells : 0;
|
CachedTileCells = bUseTileCacheWindow ? RequestedTileCells : 0;
|
||||||
CachedManagerLifetimeId = CurrentManagerLifetimeId;
|
CachedManagerLifetimeId = CurrentManagerLifetimeId;
|
||||||
CachedTerrainOps = TerrainOps;
|
CachedTerrainOps = TerrainOps;
|
||||||
|
CacheSlot->bValid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate SDF using cached rooms and tunnels (WARPED coordinates).
|
// Evaluate SDF using cached rooms and tunnels (WARPED coordinates).
|
||||||
|
|||||||
@@ -103,13 +103,15 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
int32 BandZMinVox, int32 BandZMaxVox,
|
int32 BandZMinVox, int32 BandZMaxVox,
|
||||||
int64* OutDensitySampleCount,
|
int64* OutDensitySampleCount,
|
||||||
FVoxelSharedDensityGrid* OutPreEditDensityGrid,
|
FVoxelSharedDensityGrid* OutPreEditDensityGrid,
|
||||||
const FVoxelSharedDensityGrid* InSharedDensityGrid)
|
const FVoxelSharedDensityGrid* InSharedDensityGrid,
|
||||||
|
std::vector<float>* OutDebugDensitySamples)
|
||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
|
||||||
FVoxelMeshData MeshData;
|
FVoxelMeshData MeshData;
|
||||||
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
|
||||||
if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); }
|
if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); }
|
||||||
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
|
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
|
||||||
|
if (OutDebugDensitySamples) { OutDebugDensitySamples->clear(); }
|
||||||
if (!Generator) return MeshData;
|
if (!Generator) return MeshData;
|
||||||
VoxelDensityProfile::FScopedTimer MesherProfileTimer(
|
VoxelDensityProfile::FScopedTimer MesherProfileTimer(
|
||||||
VoxelDensityProfile::EBucket::MesherGenerateMesh);
|
VoxelDensityProfile::EBucket::MesherGenerateMesh);
|
||||||
@@ -284,7 +286,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
//
|
//
|
||||||
// CellsPerAxis = nombre de cellules par axe ; GridDim = points de grille (+1).
|
// CellsPerAxis = nombre de cellules par axe ; GridDim = points de grille (+1).
|
||||||
// Le point de grille (gx,gy,gz) correspond au voxel monde (gx,gy,gz)*Step.
|
// Le point de grille (gx,gy,gz) correspond au voxel monde (gx,gy,gz)*Step.
|
||||||
// Ordre de remplissage z→y→x : garde le cache SDF (search-box) bien chaud.
|
// The grid fill order is selected below by tile extent to preserve room-cache locality.
|
||||||
// On échantillonne avec un anneau de marge de 1 point de chaque côté (indices -1..GridDim)
|
// On échantillonne avec un anneau de marge de 1 point de chaque côté (indices -1..GridDim)
|
||||||
// pour pouvoir calculer les normales par GRADIENT DE GRILLE (T1.b) — différences centrales
|
// pour pouvoir calculer les normales par GRADIENT DE GRILLE (T1.b) — différences centrales
|
||||||
// sur la grille au lieu de 6 appels densité frais par vertex. La marge utilise les mêmes
|
// sur la grille au lieu de 6 appels densité frais par vertex. La marge utilise les mêmes
|
||||||
@@ -869,56 +871,114 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
else for (int32 gz = GzLo; gz <= GzHi; gz++)
|
else
|
||||||
{
|
{
|
||||||
if (ShouldAbortWork())
|
auto BuildAxisGroups = [&](int32 AxisOrigin, int32 RegionSize)
|
||||||
{
|
{
|
||||||
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
|
std::vector<std::vector<int32>> Groups;
|
||||||
return FVoxelMeshData();
|
int32 PreviousRegion = INT32_MIN;
|
||||||
}
|
for (int32 Index = -1; Index <= GridDim; ++Index)
|
||||||
for (int32 gy = -1; gy <= GridDim; gy++)
|
|
||||||
{
|
|
||||||
if (ShouldAbortWork())
|
|
||||||
{
|
{
|
||||||
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
|
const int64 WorldCoordinate = static_cast<int64>(AxisOrigin)
|
||||||
return FVoxelMeshData();
|
+ static_cast<int64>(Index) * Step;
|
||||||
}
|
const int32 Region = FMath::FloorToInt(
|
||||||
for (int32 gx = -1; gx <= GridDim; gx++)
|
static_cast<float>(WorldCoordinate) / static_cast<float>(RegionSize));
|
||||||
{
|
if (Groups.empty() || Region != PreviousRegion)
|
||||||
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
|
|
||||||
if (bUseSharedDensityGrid)
|
|
||||||
{
|
{
|
||||||
const int32 SharedX = SharedDeltaX + gx;
|
Groups.emplace_back();
|
||||||
const int32 SharedY = SharedDeltaY + gy;
|
PreviousRegion = Region;
|
||||||
const int32 SharedZ = SharedDeltaZ + gz;
|
|
||||||
DensityGrid[LocalIndex] = ReuseGrid->Samples[
|
|
||||||
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
|
|
||||||
}
|
}
|
||||||
else
|
Groups.back().push_back(Index);
|
||||||
|
}
|
||||||
|
return Groups;
|
||||||
|
};
|
||||||
|
const int32 RoomCacheRegionSize = CHUNK_SIZE
|
||||||
|
* (VoxelGenLOD::IsUseFusedEvaluatorEnabled() ? 4 : 1);
|
||||||
|
const auto XGroups = BuildAxisGroups(OriginVoxels.X,RoomCacheRegionSize);
|
||||||
|
const auto YGroups = BuildAxisGroups(OriginVoxels.Y,RoomCacheRegionSize);
|
||||||
|
auto SampleGridPoint = [&](int32 gx, int32 gy, int32 gz)
|
||||||
|
{
|
||||||
|
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
|
||||||
|
if (bUseSharedDensityGrid)
|
||||||
|
{
|
||||||
|
const int32 SharedX = SharedDeltaX + gx;
|
||||||
|
const int32 SharedY = SharedDeltaY + gy;
|
||||||
|
const int32 SharedZ = SharedDeltaZ + gz;
|
||||||
|
DensityGrid[LocalIndex] = ReuseGrid->Samples[
|
||||||
|
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const int32 ReachBlockX = FMath::Clamp(gx, 0, CellsPerAxis - 1)
|
||||||
|
/ VoxelGenLOD::TileReachBlockCells;
|
||||||
|
const int32 ReachBlockY = FMath::Clamp(gy, 0, CellsPerAxis - 1)
|
||||||
|
/ VoxelGenLOD::TileReachBlockCells;
|
||||||
|
const int32 ReachBlockZ = FMath::Clamp(gz, 0, CellsPerAxis - 1)
|
||||||
|
/ VoxelGenLOD::TileReachBlockCells;
|
||||||
|
VoxelGenLOD::TileReachBlockIndex =
|
||||||
|
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
|
||||||
|
* ReachBlocksPerAxis + ReachBlockX;
|
||||||
|
float PreEditDensity = 0.0f;
|
||||||
|
DensityGrid[LocalIndex] = Generator->GetDensityAt(
|
||||||
|
OriginVoxels.X + gx * Step,
|
||||||
|
OriginVoxels.Y + gy * Step,
|
||||||
|
OriginVoxels.Z + gz * Step,
|
||||||
|
bCapturePreEditGrid ? &PreEditDensity : nullptr);
|
||||||
|
if (bCapturePreEditGrid)
|
||||||
{
|
{
|
||||||
// World voxel = tile origin + grid offset scaled by the cell size (Step).
|
OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity;
|
||||||
const int32 ReachBlockX = FMath::Clamp(gx, 0, CellsPerAxis - 1)
|
}
|
||||||
/ VoxelGenLOD::TileReachBlockCells;
|
if (OutDensitySampleCount)
|
||||||
const int32 ReachBlockY = FMath::Clamp(gy, 0, CellsPerAxis - 1)
|
{
|
||||||
/ VoxelGenLOD::TileReachBlockCells;
|
++(*OutDensitySampleCount);
|
||||||
const int32 ReachBlockZ = FMath::Clamp(gz, 0, CellsPerAxis - 1)
|
}
|
||||||
/ VoxelGenLOD::TileReachBlockCells;
|
}
|
||||||
VoxelGenLOD::TileReachBlockIndex =
|
};
|
||||||
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
|
// Very coarse tiles revisit the same XY cache window across a tall Z run. Keep that
|
||||||
* ReachBlocksPerAxis + ReachBlockX;
|
// vertical run contiguous; SampleGridPoint writes by lattice index, so output bits do
|
||||||
float PreEditDensity = 0.0f;
|
// not depend on this traversal. Finer tiles retain plane-first traversal, which has
|
||||||
DensityGrid[LocalIndex] = Generator->GetDensityAt(
|
// lower overhead when a tile spans only one room-graph region.
|
||||||
OriginVoxels.X + gx * Step,
|
const int64 TileExtentVoxels = static_cast<int64>(Step) * CellsPerAxis;
|
||||||
OriginVoxels.Y + gy * Step,
|
const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16;
|
||||||
OriginVoxels.Z + gz * Step,
|
if (bUseVerticalCacheTraversal)
|
||||||
bCapturePreEditGrid ? &PreEditDensity : nullptr);
|
{
|
||||||
if (bCapturePreEditGrid)
|
for (const std::vector<int32>& YGroup : YGroups)
|
||||||
|
for (const std::vector<int32>& XGroup : XGroups)
|
||||||
|
for (const int32 gy : YGroup)
|
||||||
|
for (const int32 gx : XGroup)
|
||||||
|
{
|
||||||
|
for (int32 gz = GzLo; gz <= GzHi; ++gz)
|
||||||
|
{
|
||||||
|
if (ShouldAbortWork())
|
||||||
{
|
{
|
||||||
OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity;
|
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
|
||||||
|
return FVoxelMeshData();
|
||||||
}
|
}
|
||||||
if (OutDensitySampleCount)
|
SampleGridPoint(gx,gy,gz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int32 gz = GzLo; gz <= GzHi; ++gz)
|
||||||
|
{
|
||||||
|
if (ShouldAbortWork())
|
||||||
|
{
|
||||||
|
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
|
||||||
|
return FVoxelMeshData();
|
||||||
|
}
|
||||||
|
for (const std::vector<int32>& YGroup : YGroups)
|
||||||
|
for (const std::vector<int32>& XGroup : XGroups)
|
||||||
|
for (const int32 gy : YGroup)
|
||||||
|
{
|
||||||
|
if (ShouldAbortWork())
|
||||||
{
|
{
|
||||||
++(*OutDensitySampleCount);
|
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
|
||||||
|
return FVoxelMeshData();
|
||||||
|
}
|
||||||
|
for (const int32 gx : XGroup)
|
||||||
|
{
|
||||||
|
SampleGridPoint(gx,gy,gz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -943,6 +1003,19 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
|||||||
{
|
{
|
||||||
Generator->EndDensityBlock();
|
Generator->EndDensityBlock();
|
||||||
}
|
}
|
||||||
|
if (OutDebugDensitySamples)
|
||||||
|
{
|
||||||
|
const size_t SampleCount = static_cast<size_t>(GzHi - GzLo + 1)
|
||||||
|
* static_cast<size_t>(MDim) * static_cast<size_t>(MDim);
|
||||||
|
OutDebugDensitySamples->reserve(SampleCount);
|
||||||
|
for (int32 gz = GzLo; gz <= GzHi; ++gz)
|
||||||
|
for (int32 gy = -1; gy <= GridDim; ++gy)
|
||||||
|
for (int32 gx = -1; gx <= GridDim; ++gx)
|
||||||
|
{
|
||||||
|
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
|
||||||
|
OutDebugDensitySamples->push_back(DensityGrid[LocalIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
MesherDensityGridTimer.End();
|
MesherDensityGridTimer.End();
|
||||||
|
|
||||||
if (bUseProofBlocks)
|
if (bUseProofBlocks)
|
||||||
|
|||||||
@@ -172,6 +172,28 @@ namespace VoxelDensityProfile
|
|||||||
MesherOutputAllocatedBytes,
|
MesherOutputAllocatedBytes,
|
||||||
OpBlockScratchBytes,
|
OpBlockScratchBytes,
|
||||||
OpBlockCopyBytes,
|
OpBlockCopyBytes,
|
||||||
|
SdfRebuildStrate,
|
||||||
|
SdfRebuildSeed,
|
||||||
|
SdfRebuildFingerprint,
|
||||||
|
SdfRebuildLayout,
|
||||||
|
SdfRebuildManagerLifetime,
|
||||||
|
SdfRebuildTerrainOps,
|
||||||
|
SdfRebuildFusedPolicy,
|
||||||
|
SdfRebuildTilePolicy,
|
||||||
|
SdfRebuildTileIdentity,
|
||||||
|
SdfRebuildBoundsX,
|
||||||
|
SdfRebuildBoundsY,
|
||||||
|
SdfTileWindowAccepted,
|
||||||
|
SdfTileWindowRejected,
|
||||||
|
TilePolicyKeyOrigin,
|
||||||
|
TilePolicyKeyStep,
|
||||||
|
TilePolicyKeyCells,
|
||||||
|
TilePolicyKeyFingerprint,
|
||||||
|
TilePolicyKeyLayout,
|
||||||
|
TilePolicyKeyManager,
|
||||||
|
TilePolicyKeyTerrainOps,
|
||||||
|
TilePolicyKeyFusedPath,
|
||||||
|
TilePolicyKeyContext,
|
||||||
Count
|
Count
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -433,6 +433,8 @@ private:
|
|||||||
|
|
||||||
namespace VoxelGenLOD
|
namespace VoxelGenLOD
|
||||||
{
|
{
|
||||||
|
VOXELFORGE_API void SetUseFusedEvaluatorEnabled(bool bEnabled);
|
||||||
|
VOXELFORGE_API bool IsUseFusedEvaluatorEnabled();
|
||||||
// -1 uses the measured path/LOD policy; 0/1 force the spatial broad phase off/on for an A/B
|
// -1 uses the measured path/LOD policy; 0/1 force the spatial broad phase off/on for an A/B
|
||||||
// run. The policy affects candidate enumeration only, never cache contents or ordering.
|
// run. The policy affects candidate enumeration only, never cache contents or ordering.
|
||||||
VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath);
|
VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <vector>
|
||||||
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
|
||||||
#include "VoxelGenerator.h"
|
#include "VoxelGenerator.h"
|
||||||
#include "VoxelMarchingCubesMesher.generated.h"
|
#include "VoxelMarchingCubesMesher.generated.h"
|
||||||
@@ -78,7 +79,8 @@ public:
|
|||||||
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
|
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
|
||||||
int64* OutDensitySampleCount = nullptr,
|
int64* OutDensitySampleCount = nullptr,
|
||||||
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
|
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
|
||||||
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr);
|
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr,
|
||||||
|
std::vector<float>* OutDebugDensitySamples = nullptr);
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// SERVICES (injectés par AVoxelWorld)
|
// SERVICES (injectés par AVoxelWorld)
|
||||||
|
|||||||
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.9 MiB |