diff --git a/.gitignore b/.gitignore index 6def839..7f1d4d5 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ # Keep every design / doc markdown at any depth !*.md +/Standalone/data/*.log +/Standalone/data/*.err diff --git a/Standalone/Build.ps1 b/Standalone/Build.ps1 index 7444552..f995402 100644 --- a/Standalone/Build.ps1 +++ b/Standalone/Build.ps1 @@ -54,7 +54,7 @@ $RspLines += @('/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gd Push-Location $BuildDir 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 if ($LASTEXITCODE -ne 0) { throw "Standalone MSVC build failed with exit code $LASTEXITCODE" } } finally { diff --git a/Standalone/Compat/CoreMinimal.h b/Standalone/Compat/CoreMinimal.h index ed71dc6..4372d96 100644 --- a/Standalone/Compat/CoreMinimal.h +++ b/Standalone/Compat/CoreMinimal.h @@ -8,11 +8,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -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 }; 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 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::atomicValue;}; -struct FCommandLine { static const char* Get(){return "";} }; -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;} }; +struct FCommandLine +{ + 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(Data);uint32 V=~Crc;for(int32 I=0;I>1)^(0xEDB88320u&static_cast(-static_cast(V&1)));}return ~V;} static uint32 StrCrc32(const char*S,uint32 Crc=0){return MemCrc32(S,static_cast(std::strlen(S)),Crc);} }; template using TAtomic = std::atomic; class FThreadSafeCounter { public:int32 Increment(){return ++Value;}int32 Decrement(){return --Value;}int32 GetValue()const{return Value.load();}void Reset(){Value.store(0);}private:std::atomicValue{0};}; diff --git a/Standalone/README.md b/Standalone/README.md index d5df7a2..699d61b 100644 --- a/Standalone/README.md +++ b/Standalone/README.md @@ -1,8 +1,8 @@ # VoxelForge standalone world 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 -threads, and displays them through Direct3D 11. Unreal is not linked or needed at run time. +VoxelForge density pipeline and marching-cubes mesher, streams the clipmap with six worker threads +by default, and displays it through Direct3D 11. Unreal is not linked or needed at run time. ## 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 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 @@ -23,8 +34,9 @@ The executable finds the JSON beside itself under `Standalone\data\`. - F toggles wireframe. - 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, - winding tunnel option. Either action rebuilds the clipmap. Moving three chunks from its center - also recenters and rebuilds it. + winding tunnel option; `--round-tunnels` starts in that variant. Either action rebuilds the clipmap. + Crossing a chunk recenters streaming: + tiles still desired stay resident, only new tiles are generated, and leaving tiles are dropped. - Esc exits. 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 conversion constant needed changing. Mesh vertices stay in centimetres in the copied mesher and the viewer converts them to metres at upload. -- The compatibility `TMap` and `TSet` are vector-backed containers, so lookup is linear and their - iteration follows insertion order rather than Unreal hash-table order. Owner strate slots and - passages are read from ordered JSON arrays. Perlin noise and deterministic `DetSinCos` are copied - unchanged; MSVC uses `/fp:precise` without fast-math/FMA contraction. +- The compatibility `TMap` uses an open-addressed hash index with stable pair storage; + `TSet` uses that index for lookup. Iteration follows insertion order rather than Unreal hash-table + order. Owner strate slots and passages are read from ordered JSON arrays. Perlin noise and + 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. 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 @@ -98,3 +111,136 @@ comparing later speed changes. | 5 | 23.809 | 294 | 199,503 | 2.02 M | | 6 | 296.304 | 4,704 | 4,032,866 | 32.26 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 | +|---|---| +| ![Before lighting](data/night_lighting_before.png) | ![After lighting](data/night_lighting_after.png) | + +## 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 | +|---|---| +| ![Before tunnel palette](data/night18_tunnel_view_before.png) | ![After tunnel palette](data/night19_tunnel_view_after.png) | diff --git a/Standalone/SHARED_CPP_LIBRARY_PLAN.md b/Standalone/SHARED_CPP_LIBRARY_PLAN.md index 0eba9db..e39a2ae 100644 --- a/Standalone/SHARED_CPP_LIBRARY_PLAN.md +++ b/Standalone/SHARED_CPP_LIBRARY_PLAN.md @@ -1,30 +1,74 @@ # 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 -Move the deterministic CPU world pipeline into a portable `VoxelForgeCore` library: plain voxel -and world types, math/noise, density operations and caches, cave and passage fields, strate -composition, marching cubes, and desired clipmap tile selection. Keep renderer, file loading, -asset/reflection access, gameplay actors, and task scheduling in the host applications. +Move the deterministic CPU pipeline into `VoxelForgeCore`: + +- Plain voxel, tile, vector, bounds, and resolved-world input types. +- 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 -The library should expose standard C++ value types and explicit immutable world inputs. Unreal -adapters convert `UObject` settings/assets into those inputs and convert mesh results back to -Unreal types. The standalone loader maps its JSON snapshot into the same inputs. No Unreal headers, -reflection macros, `UObject`, or host allocator should cross the library boundary. +The core should expose standard C++ value types, explicit immutable resolved inputs, and owned mesh +results. No Unreal headers, reflection macros, `UObject`, Unreal containers, renderer types, or host +allocator should cross the boundary. The plugin adapter converts settings and loaded assets into +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 -Build the core as one C++20 static library and link it from both the standalone viewer and the -VoxelForge Unreal module. The plugin keeps ownership of asset loading, worker admission, -cancellation, collision, game-thread mesh application, and per-frame upload budgets; workers call -the shared density and meshing API with immutable contexts. The standalone keeps its D3D11 renderer -and JSON loader and calls the same API. +Add a `VoxelForgeCore` C++20 target and make both builds compile or link that target from the same +source files. For Unreal, first prototype the Unreal Build Tool integration: either a small core +module that compiles the library sources or a platform-built static library with explicit build +rules. Prefer source integration if packaging a binary per Unreal platform would add maintenance. +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 -Until the move lands, keep the standalone copies synchronized from the plugin source with a small -manifest of shared files and a checked sync script. The script should compare hashes and refuse an -ambiguous two-sided edit. Run the exported 3,436-sample bitwise parity corpus after every density -change, then remove the duplicate source only when both hosts build against the shared library. +Until the common target exists, keep a manifest of mirrored files and a sync/check script that names +the canonical source for each file, compares hashes, and refuses when both copies have changed. +Avoid hand-editing only one copy without recording that the edit is pending synchronization. After +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. diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 70602cf..046aa5d 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -1,6 +1,7 @@ #include "OwnerWorldLoader.h" #include "StreamingWorld.h" #include "StandaloneResearchTunnels.h" +#include "VoxelDensityProfile.h" #ifdef TEXT #undef TEXT @@ -18,6 +19,8 @@ #include #include +#include +#include #include #include #include @@ -105,9 +108,16 @@ namespace }; struct FCrossingWork { + uint64 SubmittedJobs=0; + uint64 PendingJobs=0; + uint64 FinishedJobs=0; + uint64 CancelledQueuedJobs=0; + uint64 StaleResults=0; uint64 Tiles=0; uint64 Samples=0; double WorkerSeconds=0.0; + std::chrono::steady_clock::time_point StartTime{}; + bool bReported=false; }; FStandaloneBatchKey BatchForTile(const FVoxelTileKey& Tile) @@ -133,6 +143,9 @@ namespace std::unique_ptr World; std::unique_ptr Streamer; std::string DataPath; + bool bHasCameraOverride=false; + V3 CameraPositionOverride{}; + float CameraYawOverride=0.0f,CameraPitchOverride=0.0f; std::unordered_map ResidentTiles; std::unordered_map,FStandaloneBatchKeyHash> BatchMembers; std::unordered_map StreamingBatches; @@ -143,6 +156,7 @@ namespace uint64 GeneratedByLevel[MaxLevels+1]{}; uint64 SamplesByLevel[MaxLevels+1]{}; double WorkerSecondsByLevel[MaxLevels+1]{}; + uint64 ProfileCyclesByLevel[MaxLevels+1][VoxelDensityProfile::BucketCount]{}; std::vector TileSecondsByLevel[MaxLevels+1]; uint64 CrossingIndex=0; bool bGenerationRunning=false; @@ -154,11 +168,23 @@ namespace bool bAutoCaptureWritten=false; bool bFullDetailReported=false; bool bInitialClipmapReported=false; + bool bProfileAttribution=false; + bool bCompareEvaluators=false; + bool bParityOnly=false; + bool bDensityHash=false; + bool bTestOneChunkCrossing=false; + bool bCrossingMoveStarted=false; + bool bCrossingMoveFinished=false; + bool bCrossingClosePosted=false; + std::chrono::steady_clock::time_point CrossingMoveStopTime{}; + bool bCaptureAfterClipmap=false; + bool bQuitAfterClipmap=false; std::string CapturePath; std::chrono::steady_clock::time_point GenerationStart{}; bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true}; bool Keys[256]{}; int32 Seed=0; + int32 WorkerCount=6; int32 Width=1280,Height=800; float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f; V3 Position{}; @@ -190,7 +216,9 @@ namespace StreamingBatches.clear();BatchMembers.clear();ResidentTiles.clear(); DirtyBatchSet.clear();DesiredTiles.clear();CrossingWork.clear(); for(int32 I=0;I<=MaxLevels;++I) - {DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0;TileSecondsByLevel[I].clear();} + {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; } @@ -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{ float3 N=normalize(i.n);float3 V=normalize(Camera-i.world);float3 L=normalize(-LightDir); float Diffuse=saturate(dot(N,L));float Head=saturate(dot(N,V)); - float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0);Head=Head*Head*Atten; - 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 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.62,0.76,0.9)*Head*1.25; + float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0); + float3 Rock=lerp(float3(0.30,0.22,0.16),float3(0.48,0.43,0.34),saturate(N.z*0.5+0.5)); +// Keep the camera fill broad and low-amplitude. The old squared, high-intensity +// lobe made the 25 cm density-lattice normal variation read as horizontal bands. +Head=Head*Atten; +float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; float Fog=saturate((Dist-FogStart)/(FogEnd-FogStart)); Lit=lerp(Lit,float3(0.10,0.14,0.19),Fog);return float4(Lit,1); })"; @@ -361,9 +392,9 @@ float4 PS(O i):SV_TARGET{ return Resize(Width,Height); } - void PrintParity() + size_t PrintParity() { - if(bParityChecked||!World)return; + if(bParityChecked||!World)return 0; bParityChecked=true;size_t MismatchCount=0; std::map Kinds; for(size_t I=0;IDensitySamples.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); std::fflush(stdout); std::fflush(stdout); + return MismatchCount; } bool ConfigureWorld(std::string& Error) @@ -428,12 +460,22 @@ float4 PS(O i):SV_TARGET{ BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels); GenerationStart=std::chrono::steady_clock::now(); bFullDetailReported=false;bInitialClipmapReported=false;bAutoCaptureWritten=false; + if(bProfileAttribution) + { + VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Attribution,128); + VoxelDensityProfile::CalibrateTimer(); + VoxelDensityProfile::Reset(); + std::printf("Density stage profiling enabled (Attribution, sample interval 128).\n"); + } const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels); DesiredTiles=Desired.Tiles; for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0; for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level]; - Streamer=std::make_unique(*World,6); + Streamer=std::make_unique(*World,WorkerCount); const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex); + FCrossingWork& InitialWork=CrossingWork[CrossingIndex]; + InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs; + InitialWork.StartTime=GenerationStart; bGenerationRunning=true;bRebuildPending=false; std::printf("Streaming seed %d%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n", Seed,bRoundTunnels?" with research round/winding tunnels":"",Update.DesiredTiles, @@ -495,6 +537,15 @@ float4 PS(O i):SV_TARGET{ for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first); ++CrossingIndex; const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(CameraVoxels,ResidentKeys,CrossingIndex); + for(const auto& Pair:Update.CancelledByCrossing) + { + FCrossingWork& Work=CrossingWork[Pair.first]; + Work.PendingJobs=Work.PendingJobs>=Pair.second?Work.PendingJobs-Pair.second:0; + Work.CancelledQueuedJobs+=Pair.second; + } + FCrossingWork& Work=CrossingWork[CrossingIndex]; + Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs; + Work.StartTime=std::chrono::steady_clock::now(); BuildTargetChunk=Update.CenterChunk;bGenerationRunning=true; std::printf("[ChunkCrossing] id=%llu center=(%d,%d,%d) delta=(%d,%d,%d) desired=%d retained=%d new_jobs=%d dropped_resident=%zu cancelled_queued=%d full_detail=%d\n", static_cast(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.CancelledQueuedJobs,Update.FullResTiles); std::fflush(stdout); + ReportCrossingWork(); } 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", L,static_cast(GeneratedByLevel[L]),DesiredByLevel[L], static_cast(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(ProfileCyclesByLevel[L][I]) + *FPlatformTime::GetSecondsPerCycle64()*1000.0; + if(Ms>0.001)std::printf(" %s=%.3fms", + VoxelDensityProfile::BucketName(static_cast(I)),Ms); + } + std::printf("\n"); + } Times.clear(); } } + void ReportCrossingWork() + { + for(auto& Pair:CrossingWork) + { + FCrossingWork& Work=Pair.second; + if(Work.bReported||Work.PendingJobs!=0)continue; + Work.bReported=true; + const double AverageMs=Work.FinishedJobs>0 + ?Work.WorkerSeconds*1000.0/static_cast(Work.FinishedJobs):0.0; + const double WallSeconds=Work.StartTime.time_since_epoch().count()!=0 + ?std::chrono::duration(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(Pair.first), + static_cast(Work.SubmittedJobs), + static_cast(Work.FinishedJobs), + static_cast(Work.Tiles), + static_cast(Work.CancelledQueuedJobs), + static_cast(Work.StaleResults), + static_cast(Work.Samples)/1.0e6,Work.WorkerSeconds,WallSeconds,AverageMs); + } + } + void PumpGeneration() { if(bRebuildPending){StartGeneration();return;} @@ -632,11 +718,16 @@ float4 PS(O i):SV_TARGET{ std::vector Ready=Streamer->TakeReady(); for(FStandaloneCpuTile& Tile:Ready) { - if(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key))continue; const int32 Level=Tile.Key.Level; FCrossingWork& Crossing=CrossingWork[Tile.RequestedCrossing]; - ++Crossing.Tiles;Crossing.Samples+=Tile.DensitySamples;Crossing.WorkerSeconds+=Tile.WorkerSeconds; + if(Crossing.PendingJobs>0)--Crossing.PendingJobs; + ++Crossing.FinishedJobs;Crossing.Samples+=Tile.DensitySamples;Crossing.WorkerSeconds+=Tile.WorkerSeconds; + if(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key)) + {++Crossing.StaleResults;continue;} + ++Crossing.Tiles; ++GeneratedByLevel[Level];SamplesByLevel[Level]+=Tile.DensitySamples;WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds; + if(bProfileAttribution)for(size_t I=0;Isecond.bReported) + { + bCrossingClosePosted=true; + std::printf("[CrossingTestComplete] crossing work and its bounded uploads drained; closing this test viewer.\n"); + std::fflush(stdout); + PostMessageA(Window,WM_CLOSE,0,0); + } + } if(!bFullDetailReported&&IsFullDetailUploaded()) { bFullDetailReported=true; @@ -661,13 +765,15 @@ float4 PS(O i):SV_TARGET{ Elapsed,ResidentTiles.size(),static_cast(TotalVertices), static_cast(TotalIndices),StreamingBatches.size()); ReportTileTimings();std::fflush(stdout); + if(bQuitAfterClipmap&&CapturePath.empty())PostMessageA(Window,WM_CLOSE,0,0); } bGenerationRunning=ResidentTiles.size()!=DesiredTiles.size()||!DirtyBatchSet.empty(); } void CaptureRenderedFrame() { - if(CapturePath.empty()||bAutoCaptureWritten||!IsFullDetailUploaded())return; + const bool bReadyToCapture=bCaptureAfterClipmap?bInitialClipmapReported:IsFullDetailUploaded(); + if(CapturePath.empty()||bAutoCaptureWritten||!bReadyToCapture)return; bAutoCaptureWritten=SaveBackBufferPng(CapturePath); if(bAutoCaptureWritten) { @@ -698,6 +804,21 @@ float4 PS(O i):SV_TARGET{ { PumpGeneration(); const auto Now=std::chrono::steady_clock::now(); + if(bTestOneChunkCrossing&&bFullDetailReported&&!bCrossingMoveStarted&&!bCrossingMoveFinished) + { + bCrossingMoveStarted=true; + CrossingMoveStopTime=Now+std::chrono::milliseconds(350); + Keys[VK_SHIFT]=true;Keys['W']=true; + std::printf("[CrossingTestMove] moving forward for 350 ms after full detail.\n"); + std::fflush(stdout); + } + if(bCrossingMoveStarted&&Now>=CrossingMoveStopTime) + { + Keys[VK_SHIFT]=false;Keys['W']=false; + bCrossingMoveStarted=false;bCrossingMoveFinished=true; + std::printf("[CrossingTestMove] movement stopped; waiting for crossing work and uploads.\n"); + std::fflush(stdout); + } const float Dt=std::clamp(std::chrono::duration(Now-PreviousFrame).count(),0.0f,0.1f); PreviousFrame=Now;FrameMs=Dt*1000.0; const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F)); @@ -767,23 +888,241 @@ float4 PS(O i):SV_TARGET{ const std::filesystem::path Exe(Module); return Exe.parent_path().parent_path()/L"data"/L"owner_world.json"; } + + int RunTileBenchmark(FStandaloneOwnerWorld& World,V3 CameraPosition,int32 Level, + int32 Count,bool bProfile,bool bCompareEvaluators,bool bDensityHash) + { + const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0); + const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraVoxels); + std::vector Tiles;Tiles.reserve(static_cast(std::max(Count,1))); + for(const FVoxelTileKey& Tile:Desired.Tiles) + if(Tile.Level==Level&&static_cast(Tiles.size())(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 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(FusedDensities[I])!=std::bit_cast(OpStackDensities[I])) + { + if(FirstMismatch==SIZE_MAX){FirstMismatch=I;FirstMismatchTile=Tile;} + ++Mismatches; + MaxAbsoluteDifference=std::max(MaxAbsoluteDifference, + std::abs(FusedDensities[I]-OpStackDensities[I])); + } + } + Mismatches+=static_cast(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(ComparedSamples), + static_cast(Mismatches), + FirstMismatchTile.Coord.X,FirstMismatchTile.Coord.Y,FirstMismatchTile.Coord.Z, + static_cast(FirstMismatch),MaxAbsoluteDifference, + static_cast(MeshCountMismatches),bOriginalFused?1:0); + VoxelGenLOD::SetUseFusedEvaluatorEnabled(bOriginalFused); + std::fflush(stdout); + return Mismatches==0?0:5; + } + std::vector 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 DebugDensitySamples; + FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr, + BandMin,BandMax,&TileSamples,nullptr,nullptr, + bDensityHash?&DebugDensitySamples:nullptr); + Samples+=static_cast(FMath::Max(0,TileSamples)); + Vertices+=static_cast(Mesh.Vertices.Num()); + Indices+=static_cast(Mesh.Triangles.Num()); + if(bDensityHash) + { + for(const float Density:DebugDensitySamples) + { + DensityHash^=std::bit_cast(Density); + DensityHash*=1099511628211ull; + } + } + TileSeconds.push_back(std::chrono::duration( + std::chrono::steady_clock::now()-TileStart).count()); + } + const double TotalSeconds=std::chrono::duration( + 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(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(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(Vertices),static_cast(Indices), + bProfile?"Attribution":"off"); + if(bDensityHash) + { + std::printf("[TileDensityHash] LOD=%d tiles=%zu hash=0x%016llx\n",Level,Tiles.size(), + static_cast(DensityHash)); + } + if(bProfile) + { + std::printf("[TileProfile] LOD=%d",Level); + for(int32 I=0;I=Before.Cycles[I] + ?After.Cycles[I]-Before.Cycles[I]:0; + const double Ms=static_cast(CycleDelta) + *FPlatformTime::GetSecondsPerCycle64()*1000.0; + if(Ms>0.001)std::printf(" %s=%.3fms",VoxelDensityProfile::BucketName( + static_cast(I)),Ms); + } + std::printf("\n"); + const auto CounterDelta=[&](VoxelDensityProfile::ECounter Counter) + { + const size_t I=static_cast(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(CounterDelta(VoxelDensityProfile::ECounter::SdfCacheBuild)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTile)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTunnelCore)), + static_cast(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(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildStrate)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildSeed)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFingerprint)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildLayout)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTerrainOps)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFusedPolicy)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTilePolicy)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTileIdentity)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsX)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsY)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowAccepted)), + static_cast(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(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyOrigin)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyStep)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyCells)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFingerprint)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyLayout)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyManager)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyTerrainOps)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFusedPath)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyContext))); + } + std::fflush(stdout); + return 0; + } } int main(int Argc,char** Argv) { FApp App;App.DataPath=DefaultDataPath().string(); + std::string JoinedCommandLine; + for(int32 I=1;I(); if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;} App.Seed=App.World->Seed; + if(App.bParityOnly) + { + VoxelGenLOD::InitializeConsoleSwitches(); + return App.PrintParity()==0?0:1; + } App.Position={static_cast(App.World->ViewerStartVoxels.X*0.25), static_cast(App.World->ViewerStartVoxels.Y*0.25),static_cast(App.World->ViewerStartVoxels.Z*0.25)}; + if(App.bHasCameraOverride){App.Position=App.CameraPositionOverride;App.Yaw=App.CameraYawOverride;App.Pitch=App.CameraPitchOverride;} + if(BenchmarkLevel>=0) + return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount, + App.bProfileAttribution,App.bCompareEvaluators,App.bDensityHash); App.World.reset(); WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld"; diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 15aa52f..60ffca1 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -3,6 +3,7 @@ #include "WorldMesher.h" #include +#include #include struct FStandaloneTileKeyHash @@ -30,6 +31,7 @@ struct FStandaloneCpuTile std::vector Indices; uint64 DensitySamples=0; double WorkerSeconds=0.0; + std::array ProfileCycles{}; }; struct FStandaloneStreamingUpdate @@ -40,6 +42,7 @@ struct FStandaloneStreamingUpdate int32 NewJobs=0; int32 CancelledQueuedJobs=0; int32 FullResTiles=0; + std::vector> CancelledByCrossing; }; struct FStandaloneTileJob @@ -48,6 +51,20 @@ struct FStandaloneTileJob int32 BandZMin=MIN_int32; int32 BandZMax=MAX_int32; 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 @@ -99,6 +116,7 @@ public: explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=6) :World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,32)) { + VoxelGenLOD::InitializeConsoleSwitches(); UVoxelMarchingCubesMesher::InitializeConsoleSwitches(); } FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete; @@ -153,12 +171,20 @@ public: Desired.clear();Desired.reserve(NewDesired.Tiles.size()*2+1); for(const FVoxelTileKey& Key:NewDesired.Tiles)Desired.insert(Key); FullResTileCount=Stats.FullResTiles; + std::unordered_map CancelledByCrossing; auto QueueEnd=std::remove_if(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& Job) { if(Desired.contains(Job.Key))return false; - Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;return true; + Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs; + ++CancelledByCrossing[Job.RequestedCrossing];return true; }); 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 ? NewDesired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32; @@ -167,16 +193,12 @@ public: for(const FVoxelTileKey& Key:NewDesired.Tiles) { 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; } CurrentDesired=std::move(NewDesired); - std::sort(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& A,const FStandaloneTileJob& B) - { - const double DA=StandaloneWorldMesher::TileDistanceSquaredChunks(A.Key,CurrentDesired.CenterChunk); - const double DB=StandaloneWorldMesher::TileDistanceSquaredChunks(B.Key,CurrentDesired.CenterChunk); - return DA==DB?A.Key.LevelFullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells); const int32 Step=FMath::Max(1,Extent/Cells); 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, Job.BandZMin,Job.BandZMax,&SampleCount); + if(bProfile) + { + const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread(); + for(size_t I=0;I=Before.Cycles[Bucket] + ?After.Cycles[Bucket]-Before.Cycles[Bucket]:0; + } + } Out.DensitySamples=static_cast(FMath::Max(0,SampleCount)); Out.Vertices.reserve(static_cast(Mesh.Vertices.Num())); for(int32 I=0;I Lock(Mutex); Wake.wait(Lock,[&](){return bStop||!Queue.empty();}); 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); { diff --git a/Standalone/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Standalone/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index b9877ba..04f62f7 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -7750,7 +7750,7 @@ void VoxelCaveMorphology::BuildChunkCache( GraphLowLink.Init(INDEX_NONE, NumRooms); GraphBridges.Init(0u, GraphEdges.Num()); int32 GraphDiscoveryTime = 0; - TFunction VisitGraph = [&](int32 Node, int32 ParentEdge) + const auto VisitGraph = [&](auto&& VisitGraphSelf, int32 Node, int32 ParentEdge) -> void { GraphDiscovery[Node] = GraphDiscoveryTime; GraphLowLink[Node] = GraphDiscoveryTime; @@ -7765,7 +7765,7 @@ void VoxelCaveMorphology::BuildChunkCache( const int32 Other = Edge.RoomA == Node ? Edge.RoomB : Edge.RoomA; if (GraphDiscovery[Other] == INDEX_NONE) { - VisitGraph(Other, EdgeIndex); + VisitGraphSelf(VisitGraphSelf, Other, EdgeIndex); GraphLowLink[Node] = FMath::Min( GraphLowLink[Node], GraphLowLink[Other]); if (GraphLowLink[Other] > GraphDiscovery[Node]) @@ -7784,7 +7784,7 @@ void VoxelCaveMorphology::BuildChunkCache( { if (GraphDiscovery[Node] == INDEX_NONE) { - VisitGraph(Node, INDEX_NONE); + VisitGraph(VisitGraph, Node, INDEX_NONE); } } for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex) @@ -7803,7 +7803,7 @@ void VoxelCaveMorphology::BuildChunkCache( TArray> GraphForestEdges; GraphForestVisited.Init(0u, NumRooms); GraphForestEdges.Init(0u, GraphEdges.Num()); - TFunction VisitWalkForest = [&](int32 Node) + const auto VisitWalkForest = [&](auto&& VisitWalkForestSelf, int32 Node) -> void { GraphForestVisited[Node] = 1u; for (const int32 EdgeIndex : GraphAdjacency[Node]) @@ -7815,14 +7815,14 @@ void VoxelCaveMorphology::BuildChunkCache( continue; } GraphForestEdges[EdgeIndex] = 1u; - VisitWalkForest(Other); + VisitWalkForestSelf(VisitWalkForestSelf, Other); } }; for (int32 Node = 0; Node < NumRooms; ++Node) { if (GraphForestVisited[Node] == 0u) { - VisitWalkForest(Node); + VisitWalkForest(VisitWalkForest, Node); } } for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex) diff --git a/Standalone/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Standalone/Source/VoxelForge/Private/VoxelDensityProfile.cpp index fd68763..113d899 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -704,6 +704,28 @@ namespace VoxelDensityProfile case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes"); case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes"); 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; } return TEXT("Unknown"); diff --git a/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp b/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp index df049ac..2a83ecf 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -32,6 +32,7 @@ #endif #include +#include namespace VoxelMath { @@ -139,6 +140,24 @@ namespace TEXT("voxel.TileCacheWindowOpStackMinLOD"), GVoxelForgeTileCacheWindowOpStackMinLOD, 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; FAutoConsoleVariableRef CVarVoxelForgeSpatialIndex( @@ -879,10 +898,23 @@ void VoxelGenLOD::InitializeConsoleSwitches() VF_ParseFusedEvaluatorSwitch(); VF_ParseWormBlockSkipSwitch(); VF_ParseTileCacheWindowSwitch(); + VF_ParseRoomGraphCacheSlots(); VF_ParseSpatialIndexSwitch(); 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) { 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 // are also visible to the terrain ops block further below. - thread_local FChunkSDFCache SDFCache; - // Cache validity is tracked by the SEARCH BOX the cache was built for, NOT by chunk - // equality. Gradient-normal sampling queries at WorldX±1 (and warp displacement) can - // step a voxel outside the chunk; with chunk-equality keying that flipped the key and - // 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 - // rebuild when the query actually leaves the box. Result: one build per chunk, no thrash. - thread_local float CachedSMinX = 1.0f, CachedSMaxX = -1.0f; // start invalid (min > max) - thread_local float CachedSMinY = 0.0f, CachedSMaxY = 0.0f; - thread_local int32 CachedStrate = INT32_MIN; - thread_local uint32 CachedSeed = 0; - // ⚠️ AUDIT §C2 — `CachedFingerprint` et `CachedLayout` ci-dessous sont OBLIGATOIRES dans cette clé. - // La clé ci-dessus décrit la GÉOMÉTRIE de la fenêtre (boîte, strate, seed) et rien de ce qui - // détermine les PARAMS avec lesquels les salles ont été cuites. Comme `GetGenerationParams` - // blende à l'intérieur d'une strate (`Alpha` = f(chunk Z), et f(chunk XY) aussi en - // `Interleaved`), deux chunks voisins produisent la MÊME clé avec des params DIFFÉRENTS, et le - // deuxième se sert des salles du premier. Non déterministe entre pairs, parce que l'ordre des - // workers décide lequel est « le premier » — exactement ce que §2.6.1 interdit. - // - // Pourquoi ça ne casse PAS l'invariant de perf de §8.10 : la clé reste une BOÎTE, donc les - // sondes de gradient à `WorldX ± 1` ne font toujours pas tourner le cache. Ce qui le fait - // 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. - thread_local uint32 CachedFingerprint = 0xFFFFFFFFu; - thread_local uint32 CachedLayout = 0xFFFFFFFFu; - 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* CachedTerrainOps = nullptr; + // Keep a small bounded set of exact room-graph windows. Tall 32-chunk tiles need two slots + // for their two strata; other tile extents start with one. Every slot uses the full cache + // identity/bounds. + struct FRoomGraphCacheSlot + { + FChunkSDFCache Cache; + float SMinX = 1.0f, SMaxX = -1.0f; + float SMinY = 0.0f, SMaxY = 0.0f; + int32 Strate = INT32_MIN; + uint32 Seed = 0; + uint32 Fingerprint = 0xFFFFFFFFu; + uint32 Layout = 0xFFFFFFFFu; + bool bUsesFusedCacheWindow = false; + bool bUsesTileCacheWindow = false; + FIntVector TileOrigin = FIntVector::ZeroValue; + int32 TileStep = 0; + int32 TileCells = 0; + uint64 ManagerLifetimeId = 0; + const TArray* TerrainOps = nullptr; + uint64 LastUse = 0; + bool bValid = false; + }; + static thread_local std::array RoomGraphCacheSlots{}; + static thread_local uint64 RoomGraphCacheUseSerial = 0; + VF_ParseRoomGraphCacheSlots(); // 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 // per-sample cost on the LOD0 floor. @@ -4267,6 +4292,32 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo || CachedTilePolicyTerrainOps != TerrainOps; 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; CachedTilePolicyHasContext = bTileWindowEligible && bHasTileWindowContext; CachedTilePolicyFusedPath = bUseFusedCacheWindow; @@ -4280,6 +4331,12 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo CachedTilePolicyTerrainOps = TerrainOps; CachedTilePolicyResult = CachedTilePolicyHasContext && VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps); + if (VoxelDensityProfile::AreCountersEnabled() && CachedTilePolicyHasContext) + { + VoxelDensityProfile::AddCounter(CachedTilePolicyResult + ? VoxelDensityProfile::ECounter::SdfTileWindowAccepted + : VoxelDensityProfile::ECounter::SdfTileWindowRejected); + } } const bool bUseTileCacheWindow = CachedTilePolicyResult; @@ -4290,36 +4347,137 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo 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(CacheRegionSize); + const float ChunkMinY = CacheRegionY * static_cast(CacheRegionSize); + const float ChunkMaxX = ChunkMinX + static_cast(CacheRegionSize); + const float ChunkMaxY = ChunkMinY + static_cast(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(RequestedTileOrigin.X); + const float TileMinY = static_cast(RequestedTileOrigin.Y); + const float TileExtent = static_cast( + static_cast(RequestedTileStep) + * static_cast(RequestedTileCells)); + const float TileMaxX = TileMinX + TileExtent; + const float TileMaxY = TileMinY + TileExtent; + const float TileHalo = static_cast(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(RequestedTileStep) + * static_cast(RequestedTileCells); + const bool bNeedsTwoStrataWindows = bHasTileWindowContext + && RequestedTileExtent == static_cast(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(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*& CachedTerrainOps = CacheSlot->TerrainOps; + 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 // cached search box, or the strate/seed changed. const bool bNeedRebuild = - StrateIdx != CachedStrate || (uint32)Seed != CachedSeed || + !CacheSlot->bValid || StrateIdx != CachedStrate || (uint32)Seed != CachedSeed || ParamsFingerprint != CachedFingerprint || LayoutVersion != CachedLayout || CurrentManagerLifetimeId != CachedManagerLifetimeId || TerrainOps != CachedTerrainOps || @@ -4334,58 +4492,44 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo if (bNeedRebuild) { - // The lowered evaluator uses the same window-invariance contract as the prepared - // 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) + if (VoxelDensityProfile::AreCountersEnabled()) { - const float TileMinX = static_cast(RequestedTileOrigin.X); - const float TileMinY = static_cast(RequestedTileOrigin.Y); - const float TileExtent = static_cast( - static_cast(RequestedTileStep) - * static_cast(RequestedTileCells)); - const float TileMaxX = TileMinX + TileExtent; - const float TileMaxY = TileMinY + TileExtent; - const float TileHalo = static_cast(RequestedTileStep) + 2.0f; - const float Expansion = WarpMargin + TileHalo; - SMinX = TileMinX - Expansion; - SMinY = TileMinY - Expansion; - SMaxX = TileMaxX + Expansion; - SMaxY = TileMaxY + Expansion; + const auto CountIf = [](bool bCondition, VoxelDensityProfile::ECounter Counter) + { + if (bCondition) VoxelDensityProfile::AddCounter(Counter); + }; + CountIf(StrateIdx != CachedStrate, VoxelDensityProfile::ECounter::SdfRebuildStrate); + CountIf(static_cast(Seed) != CachedSeed, VoxelDensityProfile::ECounter::SdfRebuildSeed); + CountIf(ParamsFingerprint != CachedFingerprint, VoxelDensityProfile::ECounter::SdfRebuildFingerprint); + CountIf(LayoutVersion != CachedLayout, VoxelDensityProfile::ECounter::SdfRebuildLayout); + CountIf(CurrentManagerLifetimeId != CachedManagerLifetimeId, + VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime); + CountIf(TerrainOps != CachedTerrainOps, VoxelDensityProfile::ECounter::SdfRebuildTerrainOps); + CountIf(bUseFusedCacheWindow != CachedUsesFusedCacheWindow, + 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( SDFCache, - SMinX, SMinY, SMaxX, SMaxY, + DesiredSMinX, DesiredSMinY, DesiredSMaxX, DesiredSMaxY, Params, (uint32)Seed, StrateIdx, TerrainOps, ERoomGraphBuildSite::GeneratorTile ); UpdateTunnelCoreTileReach(SDFCache, bUseTileCacheWindow); - CachedSMinX = SMinX; CachedSMaxX = SMaxX; - CachedSMinY = SMinY; CachedSMaxY = SMaxY; + CachedSMinX = DesiredSMinX; CachedSMaxX = DesiredSMaxX; + CachedSMinY = DesiredSMinY; CachedSMaxY = DesiredSMaxY; CachedStrate = StrateIdx; CachedSeed = (uint32)Seed; CachedFingerprint = ParamsFingerprint; @@ -4398,6 +4542,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo CachedTileCells = bUseTileCacheWindow ? RequestedTileCells : 0; CachedManagerLifetimeId = CurrentManagerLifetimeId; CachedTerrainOps = TerrainOps; + CacheSlot->bValid = true; } // Evaluate SDF using cached rooms and tunnels (WARPED coordinates). diff --git a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index 1f978f5..7d930c2 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -103,13 +103,15 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 BandZMinVox, int32 BandZMaxVox, int64* OutDensitySampleCount, FVoxelSharedDensityGrid* OutPreEditDensityGrid, - const FVoxelSharedDensityGrid* InSharedDensityGrid) + const FVoxelSharedDensityGrid* InSharedDensityGrid, + std::vector* OutDebugDensitySamples) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh); FVoxelMeshData MeshData; if (OutCaptureGrid) { OutCaptureGrid->Reset(); } if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); } if (OutDensitySampleCount) { *OutDensitySampleCount = 0; } + if (OutDebugDensitySamples) { OutDebugDensitySamples->clear(); } if (!Generator) return MeshData; VoxelDensityProfile::FScopedTimer MesherProfileTimer( VoxelDensityProfile::EBucket::MesherGenerateMesh); @@ -284,7 +286,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // // 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. - // 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) // 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 @@ -869,56 +871,114 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, } #endif } - else for (int32 gz = GzLo; gz <= GzHi; gz++) + else { - if (ShouldAbortWork()) + auto BuildAxisGroups = [&](int32 AxisOrigin, int32 RegionSize) { - if (bUseOperatorBlock) { Generator->EndDensityBlock(); } - return FVoxelMeshData(); - } - for (int32 gy = -1; gy <= GridDim; gy++) - { - if (ShouldAbortWork()) + std::vector> Groups; + int32 PreviousRegion = INT32_MIN; + for (int32 Index = -1; Index <= GridDim; ++Index) { - if (bUseOperatorBlock) { Generator->EndDensityBlock(); } - return FVoxelMeshData(); - } - for (int32 gx = -1; gx <= GridDim; gx++) - { - const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1); - if (bUseSharedDensityGrid) + const int64 WorldCoordinate = static_cast(AxisOrigin) + + static_cast(Index) * Step; + const int32 Region = FMath::FloorToInt( + static_cast(WorldCoordinate) / static_cast(RegionSize)); + if (Groups.empty() || Region != PreviousRegion) { - 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]; + Groups.emplace_back(); + PreviousRegion = Region; } - 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). - 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) + OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity; + } + if (OutDensitySampleCount) + { + ++(*OutDensitySampleCount); + } + } + }; + // Very coarse tiles revisit the same XY cache window across a tall Z run. Keep that + // vertical run contiguous; SampleGridPoint writes by lattice index, so output bits do + // not depend on this traversal. Finer tiles retain plane-first traversal, which has + // lower overhead when a tile spans only one room-graph region. + const int64 TileExtentVoxels = static_cast(Step) * CellsPerAxis; + const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16; + if (bUseVerticalCacheTraversal) + { + for (const std::vector& YGroup : YGroups) + for (const std::vector& 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& YGroup : YGroups) + for (const std::vector& 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(); } + if (OutDebugDensitySamples) + { + const size_t SampleCount = static_cast(GzHi - GzLo + 1) + * static_cast(MDim) * static_cast(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(); if (bUseProofBlocks) diff --git a/Standalone/Source/VoxelForge/Public/VoxelDensityProfile.h b/Standalone/Source/VoxelForge/Public/VoxelDensityProfile.h index 810e208..fccd184 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Standalone/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -172,6 +172,28 @@ namespace VoxelDensityProfile MesherOutputAllocatedBytes, OpBlockScratchBytes, OpBlockCopyBytes, + SdfRebuildStrate, + SdfRebuildSeed, + SdfRebuildFingerprint, + SdfRebuildLayout, + SdfRebuildManagerLifetime, + SdfRebuildTerrainOps, + SdfRebuildFusedPolicy, + SdfRebuildTilePolicy, + SdfRebuildTileIdentity, + SdfRebuildBoundsX, + SdfRebuildBoundsY, + SdfTileWindowAccepted, + SdfTileWindowRejected, + TilePolicyKeyOrigin, + TilePolicyKeyStep, + TilePolicyKeyCells, + TilePolicyKeyFingerprint, + TilePolicyKeyLayout, + TilePolicyKeyManager, + TilePolicyKeyTerrainOps, + TilePolicyKeyFusedPath, + TilePolicyKeyContext, Count }; diff --git a/Standalone/Source/VoxelForge/Public/VoxelGenerator.h b/Standalone/Source/VoxelForge/Public/VoxelGenerator.h index d51da90..a6715d9 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Standalone/Source/VoxelForge/Public/VoxelGenerator.h @@ -433,6 +433,8 @@ private: 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 // run. The policy affects candidate enumeration only, never cache contents or ordering. VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath); diff --git a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index c0ad880..c741acf 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -13,6 +13,7 @@ #include "CoreMinimal.h" #include +#include #include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc. #include "VoxelGenerator.h" #include "VoxelMarchingCubesMesher.generated.h" @@ -78,7 +79,8 @@ public: int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX, int64* OutDensitySampleCount = nullptr, FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr, - const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr); + const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr, + std::vector* OutDebugDensitySamples = nullptr); //========================================================================= // SERVICES (injectés par AVoxelWorld) diff --git a/Standalone/data/night17_full_clipmap.png b/Standalone/data/night17_full_clipmap.png new file mode 100644 index 0000000..0d572a1 Binary files /dev/null and b/Standalone/data/night17_full_clipmap.png differ diff --git a/Standalone/data/night18_round_before.png b/Standalone/data/night18_round_before.png new file mode 100644 index 0000000..4158220 Binary files /dev/null and b/Standalone/data/night18_round_before.png differ diff --git a/Standalone/data/night18_tunnel_view_before.png b/Standalone/data/night18_tunnel_view_before.png new file mode 100644 index 0000000..c4128b8 Binary files /dev/null and b/Standalone/data/night18_tunnel_view_before.png differ diff --git a/Standalone/data/night19_tunnel_view_after.png b/Standalone/data/night19_tunnel_view_after.png new file mode 100644 index 0000000..e06d8e0 Binary files /dev/null and b/Standalone/data/night19_tunnel_view_after.png differ diff --git a/Standalone/data/night20_full_clipmap.png b/Standalone/data/night20_full_clipmap.png new file mode 100644 index 0000000..2745be1 Binary files /dev/null and b/Standalone/data/night20_full_clipmap.png differ diff --git a/Standalone/data/night_full_clipmap.png b/Standalone/data/night_full_clipmap.png new file mode 100644 index 0000000..0d572a1 Binary files /dev/null and b/Standalone/data/night_full_clipmap.png differ diff --git a/Standalone/data/night_lighting_after.png b/Standalone/data/night_lighting_after.png new file mode 100644 index 0000000..0d572a1 Binary files /dev/null and b/Standalone/data/night_lighting_after.png differ diff --git a/Standalone/data/night_lighting_before.png b/Standalone/data/night_lighting_before.png new file mode 100644 index 0000000..091a080 Binary files /dev/null and b/Standalone/data/night_lighting_before.png differ diff --git a/Standalone/data/night_profile.png b/Standalone/data/night_profile.png new file mode 100644 index 0000000..091a080 Binary files /dev/null and b/Standalone/data/night_profile.png differ diff --git a/Standalone/data/night_profile_detail.png b/Standalone/data/night_profile_detail.png new file mode 100644 index 0000000..091a080 Binary files /dev/null and b/Standalone/data/night_profile_detail.png differ