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
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-03 06:49:41 +02:00
1 parent 3f300b912a
commit 81b7d6d0ff
24 files changed
+1096 -196

No files matched your search

+2
View File
@@ -28,3 +28,5 @@
# Keep every design / doc markdown at any depth
!*.md
/Standalone/data/*.log
/Standalone/data/*.err
+1 -1
View File
@@ -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 {
+69 -2
View File
@@ -8,11 +8,13 @@
#include <bit>
#include <cassert>
#include <chrono>
#include <charconv>
#include <cctype>
#include <cstdarg>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <functional>
#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 };
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;};
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<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>;
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};};
+155 -9
View File
@@ -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) |
+61 -17
View File
@@ -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.
+349 -10
View File
@@ -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 <windowsx.h>
#include <bit>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
@@ -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<FStandaloneOwnerWorld> World;
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
std::string DataPath;
bool bHasCameraOverride=false;
V3 CameraPositionOverride{};
float CameraYawOverride=0.0f,CameraPitchOverride=0.0f;
std::unordered_map<FVoxelTileKey,FResidentTileMesh,FStandaloneTileKeyHash> ResidentTiles;
std::unordered_map<FStandaloneBatchKey,std::vector<FVoxelTileKey>,FStandaloneBatchKeyHash> BatchMembers;
std::unordered_map<FStandaloneBatchKey,FStreamingGpuBatch,FStandaloneBatchKeyHash> StreamingBatches;
@@ -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<double> 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<std::string,size_t> Kinds;
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);
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<FStandaloneStreamingGenerator>(*World,6);
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount);
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
FCrossingWork& InitialWork=CrossingWork[CrossingIndex];
InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs;
InitialWork.StartTime=GenerationStart;
bGenerationRunning=true;bRebuildPending=false;
std::printf("Streaming seed %d%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n",
Seed,bRoundTunnels?" with research round/winding tunnels":"",Update.DesiredTiles,
@@ -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<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.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<unsigned long long>(GeneratedByLevel[L]),DesiredByLevel[L],
static_cast<double>(SamplesByLevel[L])/1.0e6,WorkerSecondsByLevel[L],Average*1000.0,P50*1000.0,P95*1000.0);
if(bProfileAttribution)
{
std::printf("[TileProfile] LOD=%d",L);
for(size_t I=0;I<VoxelDensityProfile::BucketCount;++I)
{
const double Ms=static_cast<double>(ProfileCyclesByLevel[L][I])
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
if(Ms>0.001)std::printf(" %s=%.3fms",
VoxelDensityProfile::BucketName(static_cast<VoxelDensityProfile::EBucket>(I)),Ms);
}
std::printf("\n");
}
Times.clear();
}
}
void ReportCrossingWork()
{
for(auto& Pair:CrossingWork)
{
FCrossingWork& Work=Pair.second;
if(Work.bReported||Work.PendingJobs!=0)continue;
Work.bReported=true;
const double AverageMs=Work.FinishedJobs>0
?Work.WorkerSeconds*1000.0/static_cast<double>(Work.FinishedJobs):0.0;
const double WallSeconds=Work.StartTime.time_since_epoch().count()!=0
?std::chrono::duration<double>(std::chrono::steady_clock::now()-Work.StartTime).count():0.0;
std::printf("[ChunkCrossingWork] id=%llu submitted=%llu finished=%llu generated=%llu cancelled_queued=%llu stale=%llu samples=%.2fM worker_sum=%.3fs wall=%.3fs avg=%.3fms\n",
static_cast<unsigned long long>(Pair.first),
static_cast<unsigned long long>(Work.SubmittedJobs),
static_cast<unsigned long long>(Work.FinishedJobs),
static_cast<unsigned long long>(Work.Tiles),
static_cast<unsigned long long>(Work.CancelledQueuedJobs),
static_cast<unsigned long long>(Work.StaleResults),
static_cast<double>(Work.Samples)/1.0e6,Work.WorkerSeconds,WallSeconds,AverageMs);
}
}
void PumpGeneration()
{
if(bRebuildPending){StartGeneration();return;}
@@ -632,11 +718,16 @@ float4 PS(O i):SV_TARGET{
std::vector<FStandaloneCpuTile> 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;I<Tile.ProfileCycles.size();++I)
ProfileCyclesByLevel[Level][I]+=Tile.ProfileCycles[I];
TileSecondsByLevel[Level].push_back(Tile.WorkerSeconds);
FResidentTileMesh Resident;Resident.Vertices=std::move(Tile.Vertices);Resident.Indices=std::move(Tile.Indices);
Resident.Samples=Tile.DensitySamples;Resident.WorkerSeconds=Tile.WorkerSeconds;
@@ -644,7 +735,20 @@ float4 PS(O i):SV_TARGET{
const FStandaloneBatchKey Batch=BatchForTile(Tile.Key);
BatchMembers[Batch].push_back(Tile.Key);MarkBatchDirty(Batch);
}
ReportCrossingWork();
UploadDirtyBatches();
if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted
&&DirtyBatchSet.empty())
{
const auto Crossing=CrossingWork.find(1);
if(Crossing!=CrossingWork.end()&&Crossing->second.bReported)
{
bCrossingClosePosted=true;
std::printf("[CrossingTestComplete] crossing work and its bounded uploads drained; closing this test viewer.\n");
std::fflush(stdout);
PostMessageA(Window,WM_CLOSE,0,0);
}
}
if(!bFullDetailReported&&IsFullDetailUploaded())
{
bFullDetailReported=true;
@@ -661,13 +765,15 @@ float4 PS(O i):SV_TARGET{
Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices),
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
ReportTileTimings();std::fflush(stdout);
if(bQuitAfterClipmap&&CapturePath.empty())PostMessageA(Window,WM_CLOSE,0,0);
}
bGenerationRunning=ResidentTiles.size()!=DesiredTiles.size()||!DirtyBatchSet.empty();
}
void CaptureRenderedFrame()
{
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<float>(Now-PreviousFrame).count(),0.0f,0.1f);
PreviousFrame=Now;FrameMs=Dt*1000.0;
const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F));
@@ -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<FVoxelTileKey> Tiles;Tiles.reserve(static_cast<size_t>(std::max(Count,1)));
for(const FVoxelTileKey& Tile:Desired.Tiles)
if(Tile.Level==Level&&static_cast<int32>(Tiles.size())<Count)Tiles.push_back(Tile);
if(Tiles.empty())
{std::fprintf(stderr,"No LOD %d tiles are in the current desired set.\n",Level);return 4;}
VoxelGenLOD::InitializeConsoleSwitches();
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
int32 TileCacheWindow=1,TileCacheWindowMinLOD=3,RoomGraphCacheSlots=1;
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindow=",TileCacheWindow);
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindowMinLOD=",TileCacheWindowMinLOD);
FParse::Value(FCommandLine::Get(),"voxel.RoomGraphCacheSlots=",RoomGraphCacheSlots);
std::printf("[BenchmarkConfig] tile_cache_window=%d fused_min_lod=%d room_graph_cache_slots=%d command='%s'\n",
TileCacheWindow,TileCacheWindowMinLOD,RoomGraphCacheSlots,FCommandLine::Get());
VoxelDensityProfile::SetMode(bProfile?VoxelDensityProfile::EMode::Attribution
:VoxelDensityProfile::EMode::Disabled,128);
if(bProfile)VoxelDensityProfile::CalibrateTimer();
VoxelDensityProfile::Reset();
const VoxelDensityProfile::FSnapshot Before=VoxelDensityProfile::SnapshotCurrentThread();
UVoxelMarchingCubesMesher Mesher;
Mesher.SetGenerator(World.Generator.get());Mesher.bGenerateSkirts=World.bGenerateSkirts;
Mesher.SkirtCells=static_cast<float>(World.SkirtCells);Mesher.LODOctaveDrop=World.LODOctaveDrop;
const int32 BandMin=World.StrateContentCutMinLevel<9&&Desired.bHasStrate
?Desired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
const int32 BandMax=World.StrateContentCutMinLevel<9&&Desired.bHasStrate
?(Desired.StrateTopChunkZ+1)*CHUNK_SIZE:MAX_int32;
if(bCompareEvaluators)
{
const bool bOriginalFused=VoxelGenLOD::IsUseFusedEvaluatorEnabled();
uint64 Mismatches=0,ComparedSamples=0,MeshCountMismatches=0;
float MaxAbsoluteDifference=0.0f;
size_t FirstMismatch=SIZE_MAX;
FVoxelTileKey FirstMismatchTile{};
VoxelGenLOD::SetUseFusedEvaluatorEnabled(true);
for(const FVoxelTileKey& Tile:Tiles)
{
const int32 Cells=Level<World.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World.CoarseTileCells);
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
std::vector<float> FusedDensities,OpStackDensities;
int64 FusedSampleCalls=0,OpStackSampleCalls=0;
FVoxelMeshData FusedMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
BandMin,BandMax,&FusedSampleCalls,nullptr,nullptr,&FusedDensities);
VoxelGenLOD::SetUseFusedEvaluatorEnabled(false);
FVoxelMeshData OpStackMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
BandMin,BandMax,&OpStackSampleCalls,nullptr,nullptr,&OpStackDensities);
VoxelGenLOD::SetUseFusedEvaluatorEnabled(true);
if(FusedMesh.Vertices.Num()!=OpStackMesh.Vertices.Num()
|| FusedMesh.Triangles.Num()!=OpStackMesh.Triangles.Num())++MeshCountMismatches;
const size_t SharedSamples=std::min(FusedDensities.size(),OpStackDensities.size());
ComparedSamples+=SharedSamples;
for(size_t I=0;I<SharedSamples;++I)
{
if(std::bit_cast<uint32>(FusedDensities[I])!=std::bit_cast<uint32>(OpStackDensities[I]))
{
if(FirstMismatch==SIZE_MAX){FirstMismatch=I;FirstMismatchTile=Tile;}
++Mismatches;
MaxAbsoluteDifference=std::max(MaxAbsoluteDifference,
std::abs(FusedDensities[I]-OpStackDensities[I]));
}
}
Mismatches+=static_cast<uint64>(std::max(FusedDensities.size(),OpStackDensities.size())-SharedSamples);
}
std::printf("[EvaluatorCompare] LOD=%d tiles=%zu compared_samples=%llu density_mismatches=%llu first_tile=(%d,%d,%d) first_index=%llu max_abs=%.9g mesh_count_mismatch_tiles=%llu original_fused=%d\n",
Level,Tiles.size(),static_cast<unsigned long long>(ComparedSamples),
static_cast<unsigned long long>(Mismatches),
FirstMismatchTile.Coord.X,FirstMismatchTile.Coord.Y,FirstMismatchTile.Coord.Z,
static_cast<unsigned long long>(FirstMismatch),MaxAbsoluteDifference,
static_cast<unsigned long long>(MeshCountMismatches),bOriginalFused?1:0);
VoxelGenLOD::SetUseFusedEvaluatorEnabled(bOriginalFused);
std::fflush(stdout);
return Mismatches==0?0:5;
}
std::vector<double> TileSeconds;TileSeconds.reserve(Tiles.size());
uint64 Samples=0,Vertices=0,Indices=0;
uint64 DensityHash=14695981039346656037ull;
const auto TotalStart=std::chrono::steady_clock::now();
for(const FVoxelTileKey& Tile:Tiles)
{
const auto TileStart=std::chrono::steady_clock::now();
const int32 Cells=Level<World.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World.CoarseTileCells);
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
int64 TileSamples=0;
std::vector<float> DebugDensitySamples;
FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
BandMin,BandMax,&TileSamples,nullptr,nullptr,
bDensityHash?&DebugDensitySamples:nullptr);
Samples+=static_cast<uint64>(FMath::Max<int64>(0,TileSamples));
Vertices+=static_cast<uint64>(Mesh.Vertices.Num());
Indices+=static_cast<uint64>(Mesh.Triangles.Num());
if(bDensityHash)
{
for(const float Density:DebugDensitySamples)
{
DensityHash^=std::bit_cast<uint32>(Density);
DensityHash*=1099511628211ull;
}
}
TileSeconds.push_back(std::chrono::duration<double>(
std::chrono::steady_clock::now()-TileStart).count());
}
const double TotalSeconds=std::chrono::duration<double>(
std::chrono::steady_clock::now()-TotalStart).count();
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
std::sort(TileSeconds.begin(),TileSeconds.end());
const double Average=TotalSeconds/static_cast<double>(Tiles.size());
std::printf("[TileBenchmark] LOD=%d generated=%zu samples=%.2fM worker_sum=%.3fs avg=%.3fms p50=%.3fms p95=%.3fms vertices=%llu indices=%llu profiling=%s\n",
Level,Tiles.size(),static_cast<double>(Samples)/1.0e6,TotalSeconds,Average*1000.0,
TileSeconds[(TileSeconds.size()-1)*50/100]*1000.0,
TileSeconds[(TileSeconds.size()-1)*95/100]*1000.0,
static_cast<unsigned long long>(Vertices),static_cast<unsigned long long>(Indices),
bProfile?"Attribution":"off");
if(bDensityHash)
{
std::printf("[TileDensityHash] LOD=%d tiles=%zu hash=0x%016llx\n",Level,Tiles.size(),
static_cast<unsigned long long>(DensityHash));
}
if(bProfile)
{
std::printf("[TileProfile] LOD=%d",Level);
for(int32 I=0;I<VoxelDensityProfile::BucketCount;++I)
{
const uint64 CycleDelta=After.Cycles[I]>=Before.Cycles[I]
?After.Cycles[I]-Before.Cycles[I]:0;
const double Ms=static_cast<double>(CycleDelta)
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
if(Ms>0.001)std::printf(" %s=%.3fms",VoxelDensityProfile::BucketName(
static_cast<VoxelDensityProfile::EBucket>(I)),Ms);
}
std::printf("\n");
const auto CounterDelta=[&](VoxelDensityProfile::ECounter Counter)
{
const size_t I=static_cast<size_t>(Counter);
return After.Counters[I]>=Before.Counters[I]
?After.Counters[I]-Before.Counters[I]:0;
};
std::printf("[TileCounters] sdf_builds=%llu graph_tile_builds=%llu graph_core_builds=%llu op_block_builds=%llu\n",
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfCacheBuild)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTile)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTunnelCore)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockBuilds)));
std::printf("[SdfRebuildCauses] strate=%llu seed=%llu fingerprint=%llu layout=%llu manager=%llu terrain_ops=%llu fused_policy=%llu tile_policy=%llu tile_identity=%llu bounds_x=%llu bounds_y=%llu tile_window_accepted=%llu tile_window_rejected=%llu\n",
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildStrate)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildSeed)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFingerprint)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildLayout)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTerrainOps)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFusedPolicy)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTilePolicy)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTileIdentity)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsX)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsY)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowAccepted)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowRejected)));
std::printf("[TilePolicyKeyChanges] origin=%llu step=%llu cells=%llu fingerprint=%llu layout=%llu manager=%llu terrain_ops=%llu fused_path=%llu context=%llu\n",
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyOrigin)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyStep)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyCells)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFingerprint)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyLayout)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyManager)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyTerrainOps)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFusedPath)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyContext)));
}
std::fflush(stdout);
return 0;
}
}
int main(int Argc,char** Argv)
{
FApp App;App.DataPath=DefaultDataPath().string();
std::string JoinedCommandLine;
for(int32 I=1;I<Argc;++I)
{if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];}
FCommandLine::Set(JoinedCommandLine);
int32 BenchmarkLevel=-1,BenchmarkCount=64;
for(int32 I=1;I<Argc;++I)
{
const std::string_view Arg=Argv[I];
if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10));
else if(Arg=="--profile-attribution")App.bProfileAttribution=true;
else if(Arg=="--compare-evaluators")App.bCompareEvaluators=true;
else if(Arg=="--parity-only")App.bParityOnly=true;
else if(Arg=="--density-hash")App.bDensityHash=true;
else if(Arg=="--round-tunnels")App.bRoundTunnels=true;
else if(Arg=="--test-one-chunk-crossing")App.bTestOneChunkCrossing=true;
else if(Arg=="--capture-after-clipmap")App.bCaptureAfterClipmap=true;
else if(Arg=="--quit-after-clipmap")App.bQuitAfterClipmap=true;
else if(Arg.starts_with("--workers="))
{
const int32 Parsed=std::atoi(std::string(Arg.substr(10)).c_str());
App.WorkerCount=std::clamp(Parsed,1,32);
}
else if(Arg.starts_with("--benchmark-level="))
BenchmarkLevel=std::atoi(std::string(Arg.substr(18)).c_str());
else if(Arg.starts_with("--benchmark-count="))
BenchmarkCount=std::max(1,std::atoi(std::string(Arg.substr(18)).c_str()));
else if(Arg.starts_with("--camera="))
{
const std::string Values(Arg.substr(9));
float X=0,Y=0,Z=0,YawDegrees=0,PitchDegrees=0;
if(std::sscanf(Values.c_str(),"%f,%f,%f,%f,%f",&X,&Y,&Z,&YawDegrees,&PitchDegrees)==5)
{
App.bHasCameraOverride=true;App.CameraPositionOverride={X,Y,Z};
App.CameraYawOverride=YawDegrees*0.017453292519943295f;
App.CameraPitchOverride=PitchDegrees*0.017453292519943295f;
}
else {std::fprintf(stderr,"Invalid --camera value: %s\n",Values.c_str());return 2;}
}
else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg);
}
std::string Error;
App.World=std::make_unique<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}
App.Seed=App.World->Seed;
if(App.bParityOnly)
{
VoxelGenLOD::InitializeConsoleSwitches();
return App.PrintParity()==0?0:1;
}
App.Position={static_cast<float>(App.World->ViewerStartVoxels.X*0.25),
static_cast<float>(App.World->ViewerStartVoxels.Y*0.25),static_cast<float>(App.World->ViewerStartVoxels.Z*0.25)};
if(App.bHasCameraOverride){App.Position=App.CameraPositionOverride;App.Yaw=App.CameraYawOverride;App.Pitch=App.CameraPitchOverride;}
if(BenchmarkLevel>=0)
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
App.bProfileAttribution,App.bCompareEvaluators,App.bDensityHash);
App.World.reset();
WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld";
+45 -9
View File
@@ -3,6 +3,7 @@
#include "WorldMesher.h"
#include <condition_variable>
#include <array>
#include <unordered_set>
struct FStandaloneTileKeyHash
@@ -30,6 +31,7 @@ struct FStandaloneCpuTile
std::vector<uint32> Indices;
uint64 DensitySamples=0;
double WorkerSeconds=0.0;
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
};
struct FStandaloneStreamingUpdate
@@ -40,6 +42,7 @@ struct FStandaloneStreamingUpdate
int32 NewJobs=0;
int32 CancelledQueuedJobs=0;
int32 FullResTiles=0;
std::vector<std::pair<uint64,uint64>> 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<uint64,uint64> 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.Level<B.Key.Level:DA<DB;
});
std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Wake.notify_all();
return Stats;
}
@@ -215,8 +237,21 @@ private:
const int32 Cells=Level<World->FullResClipLevels?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<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.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
for(int32 I=0;I<Mesh.Vertices.Num();++I)
@@ -241,7 +276,8 @@ private:
std::unique_lock<std::mutex> 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);
{
@@ -7750,7 +7750,7 @@ void VoxelCaveMorphology::BuildChunkCache(
GraphLowLink.Init(INDEX_NONE, NumRooms);
GraphBridges.Init(0u, GraphEdges.Num());
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;
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<uint8, TInlineAllocator<128>> GraphForestEdges;
GraphForestVisited.Init(0u, NumRooms);
GraphForestEdges.Init(0u, GraphEdges.Num());
TFunction<void(int32)> 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)
@@ -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");
@@ -32,6 +32,7 @@
#endif
#include <atomic>
#include <array>
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<FStrateTerrainOpEntry>* 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<FStrateTerrainOpEntry>* TerrainOps = nullptr;
uint64 LastUse = 0;
bool bValid = false;
};
static thread_local std::array<FRoomGraphCacheSlot, 4> 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<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)
{
// 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<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;
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<uint32>(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).
@@ -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<float>* 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<std::vector<int32>> 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<int64>(AxisOrigin)
+ static_cast<int64>(Index) * Step;
const int32 Region = FMath::FloorToInt(
static_cast<float>(WorldCoordinate) / static_cast<float>(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<int64>(Step) * CellsPerAxis;
const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16;
if (bUseVerticalCacheTraversal)
{
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();
}
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();
if (bUseProofBlocks)
@@ -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
};
@@ -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);
@@ -13,6 +13,7 @@
#include "CoreMinimal.h"
#include <atomic>
#include <vector>
#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<float>* OutDebugDensitySamples = nullptr);
//=========================================================================
// SERVICES (injectés par AVoxelWorld)
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB