perf(gen): a thrashing one-slot cache cost 18.7x -- and the world is unchanged
TunnelNetwork density evaluation was 45x more expensive than Maze for identical work: 2,097,152 voxels, 64 tiles, same mesher, same machine. Profiling killed every hypothesis I brought to it -- box classification skipping 0/64 tiles was true but irrelevant (export bypasses classification), near-surface early-outs fired at 49.7%, the room cache built 340 entries for 2.74M samples, and the per-room overrides resolved once per sample rather than once per modifier. The cause was the wrapper, not the stack. A mesher tile samples an EXPANDED grid (-1..Cells+1), so one worker crosses several exact chunk coordinates while meshing a single tile. The generator's cache held only the last coordinate, so every boundary crossing rebuilt the resolved parameters, the whole 22-operator stack and the tunnel-core cache. On one representative tile: 3,675 refetches, 38.0s. The entire operator stack was 18.5s of a 4,272s total -- 0.43%. Replaced with a 128-slot direct-mapped thread_local cache keyed on owner, chunk, layout version, params fingerprint and generator type. Collisions force a safe rebuild, never a stale read. No shared mutable state, no order-dependent reduction. TunnelNetwork 344.5s / 164.267 us/vox -> 18.4s / 8.763 us/vox (18.7x) Maze 7.4s / 3.535 us/vox -> 1.08s / 0.513 us/vox ( 6.9x) same tile 38.0s -> 0.51s ; refetches 3675 -> 27 Output is BYTE-IDENTICAL, before vs after and across repeats, both archetypes: TunnelNetwork 50d6b4d0..f23126, Maze 364f7489..2fbe99. The permission to change the field was not needed. Also in this commit, from the two preceding tasks: - Explorer renders from the MESH instead of raymarching. It meshes the region once through the canonical UVoxelMarchingCubesMesher, then CPU-rasterises via a BVH. A viewpoint went from ~16 minutes to 0.155s; 8 views now cost 1.25s of drawing. No render pixel samples the density field. - One sampled grid and player-fit mask per run, shared by render/walk/export, instead of the same 368s walk computed twice. - A wall-clock budget (25 min default, hard 30 max) that stops cleanly and records truncated/truncated_during/completed_modes rather than overshooting. - ParallelFor across the measurement sampling; the suite was ~10% of a 16-thread machine. - -surfaceroughness= on the commandlet, and a surface-relief metric in export mode (edge lengths, dihedral distribution, face-vs-smooth normal divergence) so "does this look like rock" is a number, not a screenshot argument. - Smooth vertex normals in the rasteriser: FVoxelMeshData::Normals is the density-gradient normal the engine shades with, and the raster path was recomputing a face normal over it. - ScaleDiagnosis: baked candidate indices are now derived from the current corpus, so a legitimate field change stops breaking the test. NOT verified, and not claimed: - TunnelNetworkSpineEquivalence exceeded the 30-minute cap and was stopped. Inconclusive, not passing. The identical hashes are strong evidence the op-stack/legacy equivalence holds, but the test has not confirmed it. - CrossPlatformDigest passes but reports 2 samples within 1e-6 of the isosurface; FMath::Sin/Cos leaves Windows/Linux equivalence unproven. Pre-existing, not caused by this change. - The cache's memory footprint (128 entries x ~16 workers) is unmeasured. What the roughness sweep settled, for the record: SurfaceRoughness 0 -> 8 moves cost by 0.3% (163.2 / 163.2 / 163.7 us), so roughness was never the cost. It does reach the surface (dihedral p50 1.1 -> 2.2 -> 5.9), which kills my claim that the density-channel operator contributed nothing. TunnelNetwork's hard seams are present at roughness 0 (4.4% of edges past 40 deg vs Maze's 1.0%), so they are structural -- in how primitives are unioned -- not a roughness deficiency. More roughness makes them worse, not better. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
d97373c5a8
commit
e08b49a183
7 files changed
+2274
-450
No files matched your search
+28
-18
@@ -981,20 +981,23 @@ generic biome field), not just SurfaceWorld. Empty `Biomes[]` ⇒ all-zero colou
|
|||||||
- **Status:** C++ ✅ BUILT & WORKING (ticked 2026-07-27). The master material graph is still
|
- **Status:** C++ ✅ BUILT & WORKING (ticked 2026-07-27). The master material graph is still
|
||||||
editor-side work and is deliberately NOT ticked — that half is Jahni's, not the code's.
|
editor-side work and is deliberately NOT ticked — that half is Jahni's, not the code's.
|
||||||
|
|
||||||
### 8.16 Explorer commandlet — one authoritative field, three consumers
|
### 8.16 Explorer commandlet — one authoritative field, one sampled grid, three consumers
|
||||||
`-run=VoxelForgeExplore` lives in the separate `VoxelForgeEditor` module, so the commandlet and its
|
`-run=VoxelForgeExplore` lives in the separate `VoxelForgeEditor` module, so the commandlet and its
|
||||||
PNG/JSON/OBJ writers are editor/commandlet-only and cannot enter a Game or Shipping link. It creates
|
PNG/JSON/OBJ writers are editor/commandlet-only and cannot enter a Game or Shipping link. It creates
|
||||||
only transient settings, definitions, manager, generator, diff layer, and mesher objects; no authored
|
only transient settings, definitions, manager, generator, diff layer, and mesher objects; no authored
|
||||||
asset, live manager, generator, or diff layer is mutated. The transient world keeps
|
asset, live manager, generator, or diff layer is mutated. The transient world keeps
|
||||||
`WorldRadiusVoxels=0` and `InterStrateGapChunks=0`, preserving the lateral-region gate.
|
`WorldRadiusVoxels=0` and `InterStrateGapChunks=0`, preserving the lateral-region gate.
|
||||||
|
|
||||||
- **Render.** The renderer samples `UVoxelGenerator::GetDensityAt` at a fixed 0.25-voxel default
|
- **Render.** Render obtains the player-fit seed/mask from the shared measurement pass, then calls
|
||||||
step (overrideable down to 0.125), detects the MC density sign crossing, and refines it with ten
|
the canonical `UVoxelMarchingCubesMesher` once per 32³ tile for the bounded region. The aggregate
|
||||||
bisection iterations. It never treats density as an SDF and never sphere-traces. Each of three
|
mesh is merged in fixed Z/Y/X tile order, indexed into a deterministic CPU BVH, and rasterised
|
||||||
deterministic views overlays a projected 1.76 m human-height marker; the JSON records the step,
|
from eight player-fit viewpoints with a depth buffer and fixed Lambert lights. The per-pixel path
|
||||||
metre conversion, refinement count, and marker projection status.
|
never calls `GetDensityAt`; `-renderstep` and the old bisection fields remain only as compatibility
|
||||||
|
metadata. Each view overlays a projected 1.76 m human-height marker and the JSON records mesh,
|
||||||
|
acceleration, per-view raster timings, and marker projection status.
|
||||||
- **Walk.** `VF_MeasurePlayerFitWalkWithSampler` builds the existing step-1 capsule/floor-fit stencil
|
- **Walk.** `VF_MeasurePlayerFitWalkWithSampler` builds the existing step-1 capsule/floor-fit stencil
|
||||||
and uses the same six-neighbour/full-resolution route gate as the existing diagnostics. Its
|
and captures that exact sampled grid plus player-fit mask for the run. It uses the same
|
||||||
|
six-neighbour/full-resolution route gate as the existing diagnostics. Its
|
||||||
deterministic depth-first agent walk counts metres travelled (including branch backtracking),
|
deterministic depth-first agent walk counts metres travelled (including branch backtracking),
|
||||||
dead ends per 100 m, the arrival component's reachable fit volume, and traversed edges touching a
|
dead ends per 100 m, the arrival component's reachable fit volume, and traversed edges touching a
|
||||||
narrow-gap proxy below 1.5× the 0.68 m capsule width. The proxy is explicitly axis-aligned air span;
|
narrow-gap proxy below 1.5× the 0.68 m capsule width. The proxy is explicitly axis-aligned air span;
|
||||||
@@ -1002,19 +1005,26 @@ asset, live manager, generator, or diff layer is mutated. The transient world ke
|
|||||||
`MaxCells` refusal at a 32,000,000-cell default cap, with a conservative 20-byte/cell estimate
|
`MaxCells` refusal at a 32,000,000-cell default cap, with a conservative 20-byte/cell estimate
|
||||||
against a documented 768 MiB working-memory budget. Reachable fit volume is explicitly the bounded
|
against a documented 768 MiB working-memory budget. Reachable fit volume is explicitly the bounded
|
||||||
fitted route window because `WorldRadiusVoxels=0` deliberately has no finite whole-world volume.
|
fitted route window because `WorldRadiusVoxels=0` deliberately has no finite whole-world volume.
|
||||||
- **Export.** `UVoxelMarchingCubesMesher::GenerateMesh` is the only mesher. Because that canonical
|
- **Export.** `UVoxelMarchingCubesMesher::GenerateMesh` is still the only mesher. Because that
|
||||||
entry point deliberately clamps one call to a 32³ tile, a 128³ request is four-by-four-by-four
|
canonical entry point deliberately clamps one call to a 32³ tile, a 128³ request is four-by-four-
|
||||||
canonical calls whose returned vertices/triangles are streamed with an index offset; no second
|
by-four canonical calls. Their returned vertices/triangles are merged once and the same aggregate
|
||||||
mesher or density evaluator exists. The bounded result is metre-space OBJ with source winding,
|
is consumed by both OBJ export and render; no second mesher or density evaluator exists. Export
|
||||||
voxel-space UVs, normals, and ground/ceiling material groups, accompanied by a manifest containing seed,
|
reuses the walk's grid hand-off when a walk/render mode is present. The bounded result is metre-space
|
||||||
archetype, slot, bounds, and player dimensions. Tile skirts are disabled at the export boundary
|
OBJ with source winding, voxel-space UVs, normals, and a manifest containing seed, archetype, slot,
|
||||||
because they are a render seam aid that would extend beyond the manifest box. A conservative
|
bounds, and player dimensions. Tile skirts are disabled at the export boundary because they are a
|
||||||
preflight cap is checked before the canonical mesher is called.
|
render seam aid that would extend beyond the manifest box. A conservative preflight cap is checked
|
||||||
|
before the canonical mesher is called.
|
||||||
|
|
||||||
|
- **Budget.** The default wall-clock budget is 25 minutes; `-budget=` may lower or raise it up to a
|
||||||
|
hard 30-minute maximum. Setup, sampling, meshing, acceleration, rasterisation, and export check
|
||||||
|
the budget and report `truncated`, `truncated_during`, and `completed_modes` while retaining any
|
||||||
|
completed artifacts.
|
||||||
|
|
||||||
The report and export manifest use fixed-order writers and serialize twice in-process to assert the
|
The report and export manifest use fixed-order writers and serialize twice in-process to assert the
|
||||||
deterministic contract. Timings are console diagnostics only; putting wall-clock durations in the report would
|
deterministic contract. The fixed tile merge, BVH ordering, and raster traversal are deterministic;
|
||||||
break byte identity. The browser viewer remains a separate display-only consumer: it must load this
|
timings are intentionally diagnostic metadata and do not affect world or image content. The browser
|
||||||
OBJ/manifest and never evaluate density or generate geometry.
|
viewer remains a separate display-only consumer: it must load this OBJ/manifest and never evaluate
|
||||||
|
density or generate geometry.
|
||||||
|
|
||||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
|
|||||||
+12
-9
@@ -397,20 +397,23 @@ No UObject state, cache, actor, world, or PIE is required.
|
|||||||
| Symbol | Role |
|
| Symbol | Role |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin (including zero to sample the seal) or fit the XY window to two points with a margin, and results identify the resolved window/dimensions/Z range, deterministic air-component facts, walkable floor-area columns/fraction, projected surface components/share, and median clearance. |
|
| `FVoxelStrateMeasureSettings` / `FVoxelStrateMetrics` | Plain settings/result structs for bounded strate sampling and derived measurements; callers may override the interior margin (including zero to sample the seal) or fit the XY window to two points with a margin, and results identify the resolved window/dimensions/Z range, deterministic air-component facts, walkable floor-area columns/fraction, projected surface components/share, and median clearance. |
|
||||||
| `FVoxelStrateSampleGrid` | Optional one-pass `density > 0` `Air` polarity plus exact scalar `Density` capture exported by measurement functions; bounded by `MaxCells`, consumed by editor/automation previews, never retained on ordinary metrics calls. |
|
| `FVoxelStrateSampleGrid` | Optional one-pass `density > 0` `Air` polarity plus exact scalar `Density` capture; player-fit walks may also export the exact `PlayerFitMask` and count. Bounded by `MaxCells`, shared by explorer render/walk/export, and never retained on ordinary metrics calls. |
|
||||||
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; resolves either the legacy centered square or the two-point fitted AABB, and refuses invalid bounds or a grid over `MaxCells`. |
|
| `VF_MeasureStrate` | One-grid/one-flood-fill strate metrics; resolves either the legacy centered square or the two-point fitted AABB, and refuses invalid bounds or a grid over `MaxCells`. |
|
||||||
| `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. |
|
| `VF_AreConnected` | Coarse 6-connected BFS plus deterministic blocked-edge retries, with a full-resolution air recheck for every candidate route and explicit endpoint-solid, out-of-window, `NotConnectedAtThisResolution`, `CoarseLiedBudgetExhausted` (unknown), and connected outcomes; diagnostics include endpoint component facts and the retry count from that same grid. |
|
||||||
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
|
| `VF_DiagnoseConnectivity` | The same query plus each mouth's air-component size/share and exact geometric distance to the nearest cell of the other component; proximity is measured in coarse-cell coordinates and does not claim a route through solid. |
|
||||||
| `FVoxelPlayerFitWalkReport` / `VF_MeasurePlayerFitWalkWithSampler` | Editor-only experiential instrument over the existing exact `VF_` capsule/floor-fit mask: deterministic graph-walk distance, dead ends, reachable fit volume, and a labelled narrow-gap proxy; no second movement model. |
|
| `FVoxelPlayerFitWalkReport` / `VF_MeasurePlayerFitWalkWithSampler` | Editor-only experiential instrument over the existing exact `VF_` capsule/floor-fit mask: deterministic graph-walk distance, dead ends, reachable fit volume, and a labelled narrow-gap proxy; optionally exports the one sampled grid/mask for other explorer consumers; no second movement model. |
|
||||||
|
|
||||||
`VoxelForgeExploreCommandlet` builds a transient, fixed-definition world and calls the production
|
`VoxelForgeExploreCommandlet` builds a transient, fixed-definition world and calls the production
|
||||||
`UVoxelGenerator::GetDensityAt` and `UVoxelMarchingCubesMesher::GenerateMesh` paths. `render` uses
|
`UVoxelGenerator::GetDensityAt` and `UVoxelMarchingCubesMesher::GenerateMesh` paths. `walk` (or the
|
||||||
fixed density samples (default 0.25 voxel) with bisection at sign crossings instead of sphere tracing;
|
private seed walk used by render-only runs) captures one exact sampled grid and player-fit mask;
|
||||||
`walk` uses `VF_MeasurePlayerFitWalkWithSampler`; `export` calls the canonical 32³ mesher in a
|
render and export reuse that hand-off. `render` calls the canonical 32³ mesher in a deterministic
|
||||||
deterministic tile grid (four tiles per axis for the 128³ default), streams only its returned tile
|
tile grid (four tiles per axis for the 128³ default), merges the region once, builds a deterministic
|
||||||
arrays into metre-space OBJ plus `manifest.json` (canonical UVs remain voxel-space), and never retains the full region mesh. Output JSON/manifest are emitted by fixed-order writers and compared with a second
|
CPU BVH, and rasterises eight viewpoints from the mesh without per-pixel density calls. `export`
|
||||||
serialization before it is written. The commandlet keeps `WorldRadiusVoxels=0`, does not touch an
|
uses that same canonical aggregate for metre-space OBJ plus `manifest.json` (canonical UVs remain
|
||||||
authored asset or diff layer, and refuses bounded-grid/export requests before allocating them.
|
voxel-space). Output JSON/manifest are emitted by fixed-order writers and compared with a second
|
||||||
|
serialization before they are written. The commandlet keeps `WorldRadiusVoxels=0`, does not touch an
|
||||||
|
authored asset or diff layer, and enforces the default 25-minute / maximum 30-minute wall-clock
|
||||||
|
budget, reporting partial artifacts and completed modes on truncation.
|
||||||
|
|
||||||
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
|
||||||
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
||||||
|
|||||||
@@ -19,12 +19,16 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr int32 DiagnosisSeed = 0;
|
constexpr int32 DiagnosisSeed = 0;
|
||||||
constexpr int32 MazeCandidateIndex = 50;
|
// These are single-region seed-0 identities in the current corpus asset/default set. Keep the
|
||||||
// These are single-region seed-0 identities after the Maze default/range change updates the
|
// probes on real candidates rather than silently testing fixture defaults when a field changes.
|
||||||
// corpus content hash. Keep the probes on real candidates rather than silently testing fixture
|
constexpr int32 CrystalCandidateIndex = 50;
|
||||||
// defaults when a historical index moves to another family.
|
constexpr int32 FlatCandidateIndex = 28;
|
||||||
constexpr int32 TunnelCandidateIndex = 4;
|
constexpr int32 FloatingCandidateIndex = 4;
|
||||||
constexpr int32 ShaftCandidateIndex = 6;
|
constexpr int32 MazeCandidateIndex = 9;
|
||||||
|
constexpr int32 SurfaceCandidateIndex = 11;
|
||||||
|
constexpr int32 TunnelCandidateIndex = 20;
|
||||||
|
constexpr int32 UnderwaterCandidateIndex = 6;
|
||||||
|
constexpr int32 ShaftCandidateIndex = 10;
|
||||||
constexpr int32 RadialSampleMaxVoxels = 48;
|
constexpr int32 RadialSampleMaxVoxels = 48;
|
||||||
constexpr int32 FineDiagnosisMaxCells = 4000000;
|
constexpr int32 FineDiagnosisMaxCells = 4000000;
|
||||||
|
|
||||||
@@ -845,6 +849,8 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The corpus asset changed after d97373c; keep the current-corpus identity probes explicit
|
||||||
|
// while the historical H3 rows below remain comparison baselines.
|
||||||
FTestWorld World;
|
FTestWorld World;
|
||||||
World.Build(DiagnosisSeed, 2, true, 8);
|
World.Build(DiagnosisSeed, 2, true, 8);
|
||||||
if (!World.IsValid())
|
if (!World.IsValid())
|
||||||
@@ -864,11 +870,14 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
|||||||
Corpus, DiagnosisSeed, TunnelCandidateIndex, false);
|
Corpus, DiagnosisSeed, TunnelCandidateIndex, false);
|
||||||
const FVoxelStrateComposerCandidate ShaftCandidate = VF_RollStrateCandidate(
|
const FVoxelStrateComposerCandidate ShaftCandidate = VF_RollStrateCandidate(
|
||||||
Corpus, DiagnosisSeed, ShaftCandidateIndex, false);
|
Corpus, DiagnosisSeed, ShaftCandidateIndex, false);
|
||||||
TestTrue(TEXT("seed 0 index 50 is a valid Maze candidate"),
|
TestTrue(FString::Printf(TEXT("seed %d index %d is a valid Maze candidate"),
|
||||||
|
DiagnosisSeed, MazeCandidateIndex),
|
||||||
MazeCandidate.bValid && MazeCandidate.Archetype == ECaveGeneratorType::Maze);
|
MazeCandidate.bValid && MazeCandidate.Archetype == ECaveGeneratorType::Maze);
|
||||||
TestTrue(TEXT("seed 0 index 61 is a valid TunnelNetwork candidate"),
|
TestTrue(FString::Printf(TEXT("seed %d index %d is a valid TunnelNetwork candidate"),
|
||||||
|
DiagnosisSeed, TunnelCandidateIndex),
|
||||||
TunnelCandidate.bValid && TunnelCandidate.Archetype == ECaveGeneratorType::TunnelNetwork);
|
TunnelCandidate.bValid && TunnelCandidate.Archetype == ECaveGeneratorType::TunnelNetwork);
|
||||||
TestTrue(TEXT("seed 0 index 34 is a valid VerticalShafts candidate"),
|
TestTrue(FString::Printf(TEXT("seed %d index %d is a valid VerticalShafts candidate"),
|
||||||
|
DiagnosisSeed, ShaftCandidateIndex),
|
||||||
ShaftCandidate.bValid && ShaftCandidate.Archetype == ECaveGeneratorType::VerticalShafts);
|
ShaftCandidate.bValid && ShaftCandidate.Archetype == ECaveGeneratorType::VerticalShafts);
|
||||||
|
|
||||||
// H1 — direct GetDensityAt radial crossings. The Maze is the fourth one-based layout slot
|
// H1 — direct GetDensityAt radial crossings. The Maze is the fourth one-based layout slot
|
||||||
@@ -1151,6 +1160,8 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
|||||||
PartABase.InteriorMarginVoxels = -1;
|
PartABase.InteriorMarginVoxels = -1;
|
||||||
|
|
||||||
FGeneratorSampler GeneratorSampler(*World.Generator);
|
FGeneratorSampler GeneratorSampler(*World.Generator);
|
||||||
|
const FString MazeDiagnosisLabel = FString::Printf(
|
||||||
|
TEXT("Maze seed=%d index=%d slot=4"), DiagnosisSeed, MazeCandidateIndex);
|
||||||
auto RunPartAWindow =
|
auto RunPartAWindow =
|
||||||
[&](const FString& WindowLabel, const FVoxelStrateMeasureSettings& Settings)
|
[&](const FString& WindowLabel, const FVoxelStrateMeasureSettings& Settings)
|
||||||
{
|
{
|
||||||
@@ -1161,7 +1172,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
|||||||
VF_BoundarySealForCandidate(MazeCandidate),
|
VF_BoundarySealForCandidate(MazeCandidate),
|
||||||
ArrivalPoint, DeparturePoint, Settings, &Metrics);
|
ArrivalPoint, DeparturePoint, Settings, &Metrics);
|
||||||
VF_ReportPlayerFitRow(
|
VF_ReportPlayerFitRow(
|
||||||
*this, TEXT("Maze seed=0 index=50 slot=4"), WindowLabel,
|
*this, *MazeDiagnosisLabel, WindowLabel,
|
||||||
Settings.SampleStep, Metrics, Diagnostics);
|
Settings.SampleStep, Metrics, Diagnostics);
|
||||||
return TTuple<FVoxelStrateMetrics, FVoxelConnectivityDiagnostics>(
|
return TTuple<FVoxelStrateMetrics, FVoxelConnectivityDiagnostics>(
|
||||||
MoveTemp(Metrics), Diagnostics);
|
MoveTemp(Metrics), Diagnostics);
|
||||||
@@ -1213,9 +1224,8 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// H3 — the eight fixed showcase identities, measured at step 1 on the topology-aware fitted
|
// H3 — the eight fixed current-corpus identities, measured at step 1 on the topology-aware
|
||||||
// window. Origin-rooted families include (0,0) before the MaxCells refusal check.
|
// fitted window. Origin-rooted families include (0,0) before the MaxCells refusal check.
|
||||||
// The Maze identity is intentionally the requested seed 0 / index 50 / fourth one-based slot.
|
|
||||||
struct FShowcaseSelection
|
struct FShowcaseSelection
|
||||||
{
|
{
|
||||||
ECaveGeneratorType Archetype;
|
ECaveGeneratorType Archetype;
|
||||||
@@ -1225,14 +1235,14 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
|
|||||||
const TCHAR* Label;
|
const TCHAR* Label;
|
||||||
};
|
};
|
||||||
const FShowcaseSelection Selections[] = {
|
const FShowcaseSelection Selections[] = {
|
||||||
{ ECaveGeneratorType::CrystalChamber, 0, 28, FTestWorld::SlotFlatPlain, TEXT("CrystalChamber") },
|
{ ECaveGeneratorType::CrystalChamber, 0, CrystalCandidateIndex, FTestWorld::SlotFlatPlain, TEXT("CrystalChamber") },
|
||||||
{ ECaveGeneratorType::FlatPlain, 0, 37, FTestWorld::SlotFlatPlain, TEXT("FlatPlain") },
|
{ ECaveGeneratorType::FlatPlain, 0, FlatCandidateIndex, FTestWorld::SlotFlatPlain, TEXT("FlatPlain") },
|
||||||
{ ECaveGeneratorType::FloatingIslands, 0, 9, FTestWorld::SlotFlatPlain, TEXT("FloatingIslands") },
|
{ ECaveGeneratorType::FloatingIslands, 0, FloatingCandidateIndex, FTestWorld::SlotFlatPlain, TEXT("FloatingIslands") },
|
||||||
{ ECaveGeneratorType::Maze, 0, 50, FTestWorld::SlotMaze, TEXT("Maze") },
|
{ ECaveGeneratorType::Maze, 0, MazeCandidateIndex, FTestWorld::SlotMaze, TEXT("Maze") },
|
||||||
{ ECaveGeneratorType::SurfaceWorld, 0, 27, FTestWorld::SlotSurfaceWorld, TEXT("SurfaceWorld") },
|
{ ECaveGeneratorType::SurfaceWorld, 0, SurfaceCandidateIndex, FTestWorld::SlotSurfaceWorld, TEXT("SurfaceWorld") },
|
||||||
{ ECaveGeneratorType::TunnelNetwork, 0, 4, FTestWorld::SlotFlatPlain, TEXT("TunnelNetwork") },
|
{ ECaveGeneratorType::TunnelNetwork, 0, TunnelCandidateIndex, FTestWorld::SlotFlatPlain, TEXT("TunnelNetwork") },
|
||||||
{ ECaveGeneratorType::Underwater, 0, 16, FTestWorld::SlotFlatPlain, TEXT("Underwater") },
|
{ ECaveGeneratorType::Underwater, 0, UnderwaterCandidateIndex,FTestWorld::SlotFlatPlain, TEXT("Underwater") },
|
||||||
{ ECaveGeneratorType::VerticalShafts, 0, 6, FTestWorld::SlotFlatPlain, TEXT("VerticalShafts") },
|
{ ECaveGeneratorType::VerticalShafts, 0, ShaftCandidateIndex, FTestWorld::SlotFlatPlain, TEXT("VerticalShafts") },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Baseline rows from the pre-fix fitted-window report. The supplied VerticalShafts row is
|
// Baseline rows from the pre-fix fitted-window report. The supplied VerticalShafts row is
|
||||||
|
|||||||
@@ -477,6 +477,41 @@ namespace
|
|||||||
};
|
};
|
||||||
|
|
||||||
static thread_local FTunnelCoreCacheState GTunnelCoreCache;
|
static thread_local FTunnelCoreCacheState GTunnelCoreCache;
|
||||||
|
|
||||||
|
// A mesher tile samples an expanded grid (-1..Cells+1), so one worker visits several exact
|
||||||
|
// chunk coordinates even while it is generating one tile. The old CP_* cache retained only
|
||||||
|
// the last coordinate and consequently rebuilt the whole TunnelNetwork stack whenever the
|
||||||
|
// sampling cursor crossed a chunk boundary. Keep a deterministic direct-mapped set of states
|
||||||
|
// instead: collisions only cause a safe rebuild, never a stale read or an order-dependent
|
||||||
|
// result. Native TunnelNetwork/Underwater states are immutable after construction; the active
|
||||||
|
// pointer below lets the hot sample path read the stored stack and tunnel-core cache without
|
||||||
|
// moving them on every voxel.
|
||||||
|
constexpr int32 TunnelDensityCacheSlotCount = 128;
|
||||||
|
|
||||||
|
struct FTunnelNetworkDensityCacheEntry
|
||||||
|
{
|
||||||
|
bool bValid = false;
|
||||||
|
uint64 OwnerId = 0;
|
||||||
|
FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
|
uint32 LayoutVersion = 0xFFFFFFFFu;
|
||||||
|
ECaveGeneratorType GenType = ECaveGeneratorType::TunnelNetwork;
|
||||||
|
FStrateGenerationParams Tunnel;
|
||||||
|
uint32 TunnelFingerprint = 0xFFFFFFFFu;
|
||||||
|
FStrateDisturbanceParams Disturbance;
|
||||||
|
bool bUseOpStack = false;
|
||||||
|
FVoxelOpStack OpStack;
|
||||||
|
FTunnelCoreCacheState TunnelCore;
|
||||||
|
};
|
||||||
|
|
||||||
|
FORCEINLINE int32 TunnelDensityCacheSlot(const FIntVector& Chunk)
|
||||||
|
{
|
||||||
|
uint32 Hash = static_cast<uint32>(Chunk.X) * 0x9E3779B9u;
|
||||||
|
Hash ^= static_cast<uint32>(Chunk.Y) * 0x85EBCA6Bu;
|
||||||
|
Hash ^= static_cast<uint32>(Chunk.Z) * 0xC2B2AE35u;
|
||||||
|
Hash ^= Hash >> 16;
|
||||||
|
return static_cast<int32>(Hash & (TunnelDensityCacheSlotCount - 1));
|
||||||
|
}
|
||||||
|
|
||||||
void PrepareTunnelCoreCache(
|
void PrepareTunnelCoreCache(
|
||||||
const UVoxelStrateManager& Manager,
|
const UVoxelStrateManager& Manager,
|
||||||
const FIntVector& ChunkCoord,
|
const FIntVector& ChunkCoord,
|
||||||
@@ -702,7 +737,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
FMath::FloorToInt(WorldZ / CHUNK_SIZE)
|
FMath::FloorToInt(WorldZ / CHUNK_SIZE)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (StrateManager && StrateManager->IsGapChunk(ChunkCoord))
|
const bool bIsGapChunk = StrateManager && StrateManager->IsGapChunk(ChunkCoord);
|
||||||
|
|
||||||
|
if (StrateManager && bIsGapChunk)
|
||||||
{
|
{
|
||||||
// SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel
|
// SOLID BEDROCK gap between two strates. The auto-carved passages still tunnel
|
||||||
// through it, but the (0,0) descent stays solid here so the player digs the gap
|
// through it, but the (0,0) descent stays solid here so the player digs the gap
|
||||||
@@ -762,6 +799,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
||||||
thread_local FVoxelOpStack CP_OpStack;
|
thread_local FVoxelOpStack CP_OpStack;
|
||||||
thread_local bool CP_UseOpStack = false;
|
thread_local bool CP_UseOpStack = false;
|
||||||
|
thread_local FTunnelNetworkDensityCacheEntry CP_TunnelDensityCache[TunnelDensityCacheSlotCount];
|
||||||
|
thread_local FTunnelNetworkDensityCacheEntry* CP_ActiveTunnelDensityCache = nullptr;
|
||||||
// Cooked-season recipes and editor candidate recipes share this immutable worker-local
|
// Cooked-season recipes and editor candidate recipes share this immutable worker-local
|
||||||
// hand-off. Recipe materialisation is runtime; rolling/selection remains editor-only.
|
// hand-off. Recipe materialisation is runtime; rolling/selection remains editor-only.
|
||||||
thread_local bool CP_UseCustomRecipe = false;
|
thread_local bool CP_UseCustomRecipe = false;
|
||||||
@@ -778,9 +817,50 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
||||||
const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId;
|
FVoxelOpStack* ActiveOpStack = &CP_OpStack;
|
||||||
if (bOwnerChanged || ChunkCoord != CP_Chunk || LayoutVersion != CP_Version)
|
FTunnelCoreCacheState* ActiveTunnelCoreCache = >unnelCoreCache;
|
||||||
|
bool bLoadedTunnelDensityCache = false;
|
||||||
|
|
||||||
|
if (!bIsGapChunk)
|
||||||
{
|
{
|
||||||
|
FTunnelNetworkDensityCacheEntry& CachedEntry =
|
||||||
|
CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)];
|
||||||
|
const bool bCacheKeyMatches = CachedEntry.bValid
|
||||||
|
&& CachedEntry.OwnerId == DensityCacheOwnerId
|
||||||
|
&& CachedEntry.Chunk == ChunkCoord
|
||||||
|
&& CachedEntry.LayoutVersion == LayoutVersion
|
||||||
|
&& (CachedEntry.GenType == ECaveGeneratorType::TunnelNetwork
|
||||||
|
|| CachedEntry.GenType == ECaveGeneratorType::Underwater);
|
||||||
|
if (bCacheKeyMatches)
|
||||||
|
{
|
||||||
|
if (CP_ActiveTunnelDensityCache != &CachedEntry)
|
||||||
|
{
|
||||||
|
CP_OwnerId = CachedEntry.OwnerId;
|
||||||
|
CP_Version = CachedEntry.LayoutVersion;
|
||||||
|
CP_Chunk = CachedEntry.Chunk;
|
||||||
|
CP_GenType = CachedEntry.GenType;
|
||||||
|
CP_Tunnel = CachedEntry.Tunnel;
|
||||||
|
CP_TunnelFP = CachedEntry.TunnelFingerprint;
|
||||||
|
CP_Dist = CachedEntry.Disturbance;
|
||||||
|
CP_UseOpStack = CachedEntry.bUseOpStack;
|
||||||
|
CP_UseCustomRecipe = false;
|
||||||
|
#if WITH_EDITOR
|
||||||
|
CP_UseComposerRegions = false;
|
||||||
|
#endif
|
||||||
|
CP_ActiveTunnelDensityCache = &CachedEntry;
|
||||||
|
}
|
||||||
|
ActiveOpStack = &CachedEntry.OpStack;
|
||||||
|
ActiveTunnelCoreCache = &CachedEntry.TunnelCore;
|
||||||
|
bLoadedTunnelDensityCache = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId;
|
||||||
|
if (!bLoadedTunnelDensityCache
|
||||||
|
&& (bOwnerChanged || ChunkCoord != CP_Chunk || LayoutVersion != CP_Version))
|
||||||
|
{
|
||||||
|
CP_ActiveTunnelDensityCache = nullptr;
|
||||||
|
|
||||||
// La grille de biome est validée par une BOÎTE XY, qui ne dit rien du FBiomeContext
|
// La grille de biome est validée par une BOÎTE XY, qui ne dit rien du FBiomeContext
|
||||||
// ayant servi à classer ses cellules : sur un changement de version elle est périmée
|
// ayant servi à classer ses cellules : sur un changement de version elle est périmée
|
||||||
// même si la boîte couvre encore la requête. Même invalidation quand le propriétaire
|
// même si la boîte couvre encore la requête. Même invalidation quand le propriétaire
|
||||||
@@ -976,6 +1056,43 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool bNativeTunnelCacheCandidate = !CP_UseCustomRecipe
|
||||||
|
&& (CP_GenType == ECaveGeneratorType::TunnelNetwork
|
||||||
|
|| CP_GenType == ECaveGeneratorType::Underwater);
|
||||||
|
#if WITH_EDITOR
|
||||||
|
const bool bCanStoreTunnelDensityCache = bNativeTunnelCacheCandidate
|
||||||
|
&& !CP_UseComposerRegions;
|
||||||
|
#else
|
||||||
|
const bool bCanStoreTunnelDensityCache = bNativeTunnelCacheCandidate;
|
||||||
|
#endif
|
||||||
|
if (!bLoadedTunnelDensityCache && bCanStoreTunnelDensityCache)
|
||||||
|
{
|
||||||
|
FTunnelNetworkDensityCacheEntry& CachedEntry =
|
||||||
|
CP_TunnelDensityCache[TunnelDensityCacheSlot(ChunkCoord)];
|
||||||
|
CachedEntry.bValid = false;
|
||||||
|
CachedEntry.OwnerId = DensityCacheOwnerId;
|
||||||
|
CachedEntry.Chunk = ChunkCoord;
|
||||||
|
CachedEntry.LayoutVersion = LayoutVersion;
|
||||||
|
CachedEntry.GenType = CP_GenType;
|
||||||
|
CachedEntry.Tunnel = CP_Tunnel;
|
||||||
|
CachedEntry.TunnelFingerprint = CP_TunnelFP;
|
||||||
|
CachedEntry.Disturbance = CP_Dist;
|
||||||
|
CachedEntry.bUseOpStack = CP_UseOpStack;
|
||||||
|
CachedEntry.OpStack = MoveTemp(CP_OpStack);
|
||||||
|
CachedEntry.TunnelCore = MoveTemp(GTunnelCoreCache);
|
||||||
|
GTunnelCoreCache = FTunnelCoreCacheState();
|
||||||
|
CachedEntry.bValid = true;
|
||||||
|
CP_ActiveTunnelDensityCache = &CachedEntry;
|
||||||
|
ActiveOpStack = &CachedEntry.OpStack;
|
||||||
|
ActiveTunnelCoreCache = &CachedEntry.TunnelCore;
|
||||||
|
}
|
||||||
|
else if (!bLoadedTunnelDensityCache)
|
||||||
|
{
|
||||||
|
ActiveOpStack = &CP_OpStack;
|
||||||
|
ActiveTunnelCoreCache = >unnelCoreCache;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention
|
// Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention
|
||||||
@@ -983,7 +1100,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// de diff qui suivent ne voient aucune différence.
|
// de diff qui suivent ne voient aucune différence.
|
||||||
if (CP_UseOpStack)
|
if (CP_UseOpStack)
|
||||||
{
|
{
|
||||||
Result = CP_OpStack.EvalMC(WorldX, WorldY, WorldZ);
|
Result = ActiveOpStack->EvalMC(WorldX, WorldY, WorldZ);
|
||||||
}
|
}
|
||||||
else switch (CP_GenType)
|
else switch (CP_GenType)
|
||||||
{
|
{
|
||||||
@@ -1153,11 +1270,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
|
// bridge over a graph tunnel. Reassert the native cached tunnel core here, after every
|
||||||
// solid floor writer but before the global XY seal. This cache is built once per chunk,
|
// solid floor writer but before the global XY seal. This cache is built once per chunk,
|
||||||
// never once per voxel.
|
// never once per voxel.
|
||||||
if (GTunnelCoreCache.bValid)
|
if (ActiveTunnelCoreCache->bValid)
|
||||||
{
|
{
|
||||||
const bool bTunnelSupportFloor =
|
const bool bTunnelSupportFloor =
|
||||||
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
|
||||||
WorldX, WorldY, WorldZ, GTunnelCoreCache.Cache);
|
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache);
|
||||||
if (bTunnelSupportFloor)
|
if (bTunnelSupportFloor)
|
||||||
{
|
{
|
||||||
Result = FMath::Min(
|
Result = FMath::Min(
|
||||||
@@ -1165,7 +1282,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
-FMath::Max(LandingBaseDensity * 2.0f, 1.0f));
|
||||||
}
|
}
|
||||||
const float CoreSDF = VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
const float CoreSDF = VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
|
||||||
WorldX, WorldY, WorldZ, GTunnelCoreCache.Cache);
|
WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache);
|
||||||
if (!bTunnelSupportFloor
|
if (!bTunnelSupportFloor
|
||||||
&& CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
|
&& CoreSDF < -VoxelPassageGeometry::CaveTunnelAirCoreInsetVoxels)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -455,6 +455,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
// only when it explicitly requested a capture.
|
// only when it explicitly requested a capture.
|
||||||
Destination.Air = MoveTemp(Source.Air);
|
Destination.Air = MoveTemp(Source.Air);
|
||||||
Destination.Density = MoveTemp(Source.Density);
|
Destination.Density = MoveTemp(Source.Density);
|
||||||
|
Destination.PlayerFitMask.Reset();
|
||||||
|
Destination.PlayerFitCellCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DecodeIndex(const FSampleGrid& Grid, int32 Index, int32& OutX, int32& OutY, int32& OutZ)
|
void DecodeIndex(const FSampleGrid& Grid, int32 Index, int32& OutX, int32& OutY, int32& OutZ)
|
||||||
@@ -1374,6 +1376,37 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
? 0.0f : static_cast<float>(MinimumDistance);
|
? 0.0f : static_cast<float>(MinimumDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int64 CountAirCells(
|
||||||
|
const FSampleGrid& Grid,
|
||||||
|
const TArray<uint8>* EligibilityMask = nullptr)
|
||||||
|
{
|
||||||
|
TArray<int64> AirCountsByZ;
|
||||||
|
AirCountsByZ.Init(0, Grid.NumZ);
|
||||||
|
ParallelFor(Grid.NumZ, [&](int32 Z)
|
||||||
|
{
|
||||||
|
int64 Count = 0;
|
||||||
|
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||||
|
{
|
||||||
|
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||||
|
{
|
||||||
|
const int32 Cell = Grid.Index(X, Y, Z);
|
||||||
|
Count += Grid.Air[Cell] != 0u
|
||||||
|
&& (EligibilityMask == nullptr || (*EligibilityMask)[Cell] != 0u)
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AirCountsByZ[Z] = Count;
|
||||||
|
});
|
||||||
|
|
||||||
|
int64 Total = 0;
|
||||||
|
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
||||||
|
{
|
||||||
|
Total += AirCountsByZ[Z];
|
||||||
|
}
|
||||||
|
return Total;
|
||||||
|
}
|
||||||
|
|
||||||
void BuildPlayerFitMetrics(
|
void BuildPlayerFitMetrics(
|
||||||
const FSampleGrid& Grid,
|
const FSampleGrid& Grid,
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
@@ -1453,10 +1486,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
OutMetrics.SampledMinY = Grid.MinY;
|
OutMetrics.SampledMinY = Grid.MinY;
|
||||||
OutMetrics.SampledMaxY = Grid.MaxY;
|
OutMetrics.SampledMaxY = Grid.MaxY;
|
||||||
OutMetrics.NumSampled = Grid.CellCount;
|
OutMetrics.NumSampled = Grid.CellCount;
|
||||||
for (const uint8 bAir : Grid.Air)
|
OutMetrics.NumAir = CountAirCells(Grid);
|
||||||
{
|
|
||||||
OutMetrics.NumAir += bAir != 0u ? 1 : 0;
|
|
||||||
}
|
|
||||||
OutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - OutMetrics.NumAir;
|
OutMetrics.NumSolid = static_cast<int64>(Grid.CellCount) - OutMetrics.NumAir;
|
||||||
OutMetrics.AirFraction = OutMetrics.NumSampled > 0
|
OutMetrics.AirFraction = OutMetrics.NumSampled > 0
|
||||||
? static_cast<float>(static_cast<double>(OutMetrics.NumAir)
|
? static_cast<float>(static_cast<double>(OutMetrics.NumAir)
|
||||||
@@ -1487,11 +1517,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
TArray<int32> Components;
|
TArray<int32> Components;
|
||||||
int32 NumComponents = 0;
|
int32 NumComponents = 0;
|
||||||
|
|
||||||
int64 NumAir = 0;
|
const int64 NumAir = CountAirCells(Grid);
|
||||||
for (const uint8 bAir : Grid.Air)
|
|
||||||
{
|
|
||||||
NumAir += bAir != 0u ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int64 LargestComponentCells = 0;
|
int64 LargestComponentCells = 0;
|
||||||
int32 LargestComponentLowestCell = INDEX_NONE;
|
int32 LargestComponentLowestCell = INDEX_NONE;
|
||||||
@@ -1537,8 +1563,21 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
TArray<uint8> WalkableColumns;
|
TArray<uint8> WalkableColumns;
|
||||||
WalkableColumns.Init(0u, static_cast<int32>(XYColumnCount64));
|
WalkableColumns.Init(0u, static_cast<int32>(XYColumnCount64));
|
||||||
|
|
||||||
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
// Candidate poses read only the immutable sampled field. Keep every worker's clearance
|
||||||
|
// values and compact column flags private, then merge by fixed Z order so the median and
|
||||||
|
// projected surface graph remain deterministic.
|
||||||
|
TArray<int64> WalkableCountsByZ;
|
||||||
|
WalkableCountsByZ.Init(0, Grid.NumZ);
|
||||||
|
TArray<TArray<int32>> ClearanceValuesByZ;
|
||||||
|
ClearanceValuesByZ.SetNum(Grid.NumZ);
|
||||||
|
TArray<TArray<uint8>> WalkableColumnsByZ;
|
||||||
|
WalkableColumnsByZ.SetNum(Grid.NumZ);
|
||||||
|
ParallelFor(Grid.NumZ, [&](int32 Z)
|
||||||
{
|
{
|
||||||
|
TArray<int32>& SliceClearanceValues = ClearanceValuesByZ[Z];
|
||||||
|
TArray<uint8>& SliceWalkableColumns = WalkableColumnsByZ[Z];
|
||||||
|
SliceWalkableColumns.Init(0u, static_cast<int32>(XYColumnCount64));
|
||||||
|
int64 SliceWalkableCount = 0;
|
||||||
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||||
{
|
{
|
||||||
for (int32 X = 0; X < Grid.NumX; ++X)
|
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||||
@@ -1563,8 +1602,8 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
if (!bHasHeadroom) continue;
|
if (!bHasHeadroom) continue;
|
||||||
|
|
||||||
++NumWalkable;
|
++SliceWalkableCount;
|
||||||
WalkableColumns[X + Grid.NumX * Y] = 1u;
|
SliceWalkableColumns[X + Grid.NumX * Y] = 1u;
|
||||||
int32 ClearanceCells = 0;
|
int32 ClearanceCells = 0;
|
||||||
while (static_cast<int64>(Z) + ClearanceCells < Grid.NumZ
|
while (static_cast<int64>(Z) + ClearanceCells < Grid.NumZ
|
||||||
&& Grid.Air[Grid.Index(X, Y, Z + ClearanceCells)] != 0u)
|
&& Grid.Air[Grid.Index(X, Y, Z + ClearanceCells)] != 0u)
|
||||||
@@ -1573,10 +1612,25 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
}
|
}
|
||||||
const int64 ClearanceVoxels = static_cast<int64>(ClearanceCells)
|
const int64 ClearanceVoxels = static_cast<int64>(ClearanceCells)
|
||||||
* Settings.SampleStep;
|
* Settings.SampleStep;
|
||||||
ClearanceValues.Add(ClearanceVoxels > INT32_MAX
|
SliceClearanceValues.Add(ClearanceVoxels > INT32_MAX
|
||||||
? INT32_MAX : static_cast<int32>(ClearanceVoxels));
|
? INT32_MAX : static_cast<int32>(ClearanceVoxels));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WalkableCountsByZ[Z] = SliceWalkableCount;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
||||||
|
{
|
||||||
|
NumWalkable += WalkableCountsByZ[Z];
|
||||||
|
const TArray<uint8>& SliceWalkableColumns = WalkableColumnsByZ[Z];
|
||||||
|
for (int32 Column = 0; Column < SliceWalkableColumns.Num(); ++Column)
|
||||||
|
{
|
||||||
|
if (SliceWalkableColumns[Column] != 0u)
|
||||||
|
{
|
||||||
|
WalkableColumns[Column] = 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClearanceValues.Append(ClearanceValuesByZ[Z]);
|
||||||
}
|
}
|
||||||
|
|
||||||
InOutMetrics.WalkableFraction = NumAir > 0
|
InOutMetrics.WalkableFraction = NumAir > 0
|
||||||
@@ -1654,15 +1708,29 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
TArray<int32> Distances;
|
TArray<int32> Distances;
|
||||||
BuildManhattanDistanceToSolid(Grid, Distances);
|
BuildManhattanDistanceToSolid(Grid, Distances);
|
||||||
|
|
||||||
|
TArray<TArray<float>> FeatureScalesByZ;
|
||||||
|
FeatureScalesByZ.SetNum(Grid.NumZ);
|
||||||
|
ParallelFor(Grid.NumZ, [&](int32 Z)
|
||||||
|
{
|
||||||
|
TArray<float>& SliceFeatureScales = FeatureScalesByZ[Z];
|
||||||
|
for (int32 Y = 0; Y < Grid.NumY; ++Y)
|
||||||
|
{
|
||||||
|
for (int32 X = 0; X < Grid.NumX; ++X)
|
||||||
|
{
|
||||||
|
const int32 Index = Grid.Index(X, Y, Z);
|
||||||
|
if (Grid.Air[Index] != 0u)
|
||||||
|
{
|
||||||
|
SliceFeatureScales.Add(static_cast<float>(Distances[Index])
|
||||||
|
* static_cast<float>(Settings.SampleStep));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
TArray<float> FeatureScales;
|
TArray<float> FeatureScales;
|
||||||
FeatureScales.Reserve(static_cast<int32>(NumAir));
|
FeatureScales.Reserve(static_cast<int32>(NumAir));
|
||||||
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
for (int32 Z = 0; Z < Grid.NumZ; ++Z)
|
||||||
{
|
{
|
||||||
if (Grid.Air[Index] != 0u)
|
FeatureScales.Append(FeatureScalesByZ[Z]);
|
||||||
{
|
|
||||||
FeatureScales.Add(static_cast<float>(Distances[Index])
|
|
||||||
* static_cast<float>(Settings.SampleStep));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
InOutMetrics.MedianFeatureScale = MedianFloat(FeatureScales);
|
InOutMetrics.MedianFeatureScale = MedianFloat(FeatureScales);
|
||||||
}
|
}
|
||||||
@@ -2220,13 +2288,7 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
int64 LargestComponentCells = 0;
|
int64 LargestComponentCells = 0;
|
||||||
int32 LargestComponentLowestCell = INDEX_NONE;
|
int32 LargestComponentLowestCell = INDEX_NONE;
|
||||||
int32 NumComponentsAtLeast1Pct = 0;
|
int32 NumComponentsAtLeast1Pct = 0;
|
||||||
int64 NumAir = 0;
|
const int64 NumAir = CountAirCells(Grid, EligibilityMask);
|
||||||
for (int32 Index = 0; Index < Grid.CellCount; ++Index)
|
|
||||||
{
|
|
||||||
NumAir += Grid.Air[Index] != 0u
|
|
||||||
&& (EligibilityMask == nullptr || (*EligibilityMask)[Index] != 0u)
|
|
||||||
? 1 : 0;
|
|
||||||
}
|
|
||||||
FloodFillAir(
|
FloodFillAir(
|
||||||
Grid,
|
Grid,
|
||||||
Components,
|
Components,
|
||||||
@@ -2284,10 +2346,16 @@ namespace VoxelStrateMeasurePrivate
|
|||||||
OutDiagnostics.StartComponentCells == LargestComponentCells;
|
OutDiagnostics.StartComponentCells == LargestComponentCells;
|
||||||
OutDiagnostics.bGoalComponentIsLargest =
|
OutDiagnostics.bGoalComponentIsLargest =
|
||||||
OutDiagnostics.GoalComponentCells == LargestComponentCells;
|
OutDiagnostics.GoalComponentCells == LargestComponentCells;
|
||||||
OutDiagnostics.StartToGoalComponentDistanceCells = DistanceToComponent(
|
TArray<float> ComponentDistances;
|
||||||
Grid, Start, GoalComponent, Components);
|
ComponentDistances.Init(0.0f, 2);
|
||||||
OutDiagnostics.GoalToStartComponentDistanceCells = DistanceToComponent(
|
ParallelFor(2, [&](int32 DistanceIndex)
|
||||||
Grid, Goal, StartComponent, Components);
|
{
|
||||||
|
ComponentDistances[DistanceIndex] = DistanceIndex == 0
|
||||||
|
? DistanceToComponent(Grid, Start, GoalComponent, Components)
|
||||||
|
: DistanceToComponent(Grid, Goal, StartComponent, Components);
|
||||||
|
});
|
||||||
|
OutDiagnostics.StartToGoalComponentDistanceCells = ComponentDistances[0];
|
||||||
|
OutDiagnostics.GoalToStartComponentDistanceCells = ComponentDistances[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
EVoxelConnectivityResult QueryConnectivity(
|
EVoxelConnectivityResult QueryConnectivity(
|
||||||
@@ -2702,7 +2770,12 @@ FVoxelStrateMetrics VF_MeasureStrate(
|
|||||||
Result.SampledMaxX = Grid.MaxX;
|
Result.SampledMaxX = Grid.MaxX;
|
||||||
Result.SampledMinY = Grid.MinY;
|
Result.SampledMinY = Grid.MinY;
|
||||||
Result.SampledMaxY = Grid.MaxY;
|
Result.SampledMaxY = Grid.MaxY;
|
||||||
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
|
TArray<uint8> PlayerFitMask;
|
||||||
|
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(
|
||||||
|
Grid,
|
||||||
|
Settings,
|
||||||
|
Result,
|
||||||
|
OutSampleGrid != nullptr ? &PlayerFitMask : nullptr);
|
||||||
Result.bValid = Result.NumSampled > 0;
|
Result.bValid = Result.NumSampled > 0;
|
||||||
if (!Result.bValid)
|
if (!Result.bValid)
|
||||||
{
|
{
|
||||||
@@ -2711,6 +2784,8 @@ FVoxelStrateMetrics VF_MeasureStrate(
|
|||||||
if (OutSampleGrid != nullptr && Result.bValid)
|
if (OutSampleGrid != nullptr && Result.bValid)
|
||||||
{
|
{
|
||||||
VoxelStrateMeasurePrivate::ExportSampleGrid(Grid, *OutSampleGrid);
|
VoxelStrateMeasurePrivate::ExportSampleGrid(Grid, *OutSampleGrid);
|
||||||
|
OutSampleGrid->PlayerFitMask = MoveTemp(PlayerFitMask);
|
||||||
|
OutSampleGrid->PlayerFitCellCount = Result.NumPlayerFitCells;
|
||||||
}
|
}
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
@@ -2748,7 +2823,12 @@ FVoxelStrateMetrics VF_MeasureStrateWithSampler(
|
|||||||
Result.SampledMaxX = Grid.MaxX;
|
Result.SampledMaxX = Grid.MaxX;
|
||||||
Result.SampledMinY = Grid.MinY;
|
Result.SampledMinY = Grid.MinY;
|
||||||
Result.SampledMaxY = Grid.MaxY;
|
Result.SampledMaxY = Grid.MaxY;
|
||||||
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(Grid, Settings, Result);
|
TArray<uint8> PlayerFitMask;
|
||||||
|
VoxelStrateMeasurePrivate::BuildMetricsFromGrid(
|
||||||
|
Grid,
|
||||||
|
Settings,
|
||||||
|
Result,
|
||||||
|
OutSampleGrid != nullptr ? &PlayerFitMask : nullptr);
|
||||||
Result.bValid = Result.NumSampled > 0;
|
Result.bValid = Result.NumSampled > 0;
|
||||||
if (!Result.bValid)
|
if (!Result.bValid)
|
||||||
{
|
{
|
||||||
@@ -2757,6 +2837,8 @@ FVoxelStrateMetrics VF_MeasureStrateWithSampler(
|
|||||||
if (OutSampleGrid != nullptr && Result.bValid)
|
if (OutSampleGrid != nullptr && Result.bValid)
|
||||||
{
|
{
|
||||||
VoxelStrateMeasurePrivate::ExportSampleGrid(Grid, *OutSampleGrid);
|
VoxelStrateMeasurePrivate::ExportSampleGrid(Grid, *OutSampleGrid);
|
||||||
|
OutSampleGrid->PlayerFitMask = MoveTemp(PlayerFitMask);
|
||||||
|
OutSampleGrid->PlayerFitCellCount = Result.NumPlayerFitCells;
|
||||||
}
|
}
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
@@ -2951,11 +3033,16 @@ bool VF_MeasurePlayerFitWalkWithSampler(
|
|||||||
const FVector& BVoxel,
|
const FVector& BVoxel,
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
FVoxelPlayerFitWalkReport& OutReport,
|
FVoxelPlayerFitWalkReport& OutReport,
|
||||||
const TArray<FVector>* ComponentProbePoints)
|
const TArray<FVector>* ComponentProbePoints,
|
||||||
|
FVoxelStrateSampleGrid* OutSampleGrid)
|
||||||
{
|
{
|
||||||
using namespace VoxelStrateMeasurePrivate;
|
using namespace VoxelStrateMeasurePrivate;
|
||||||
|
|
||||||
OutReport = FVoxelPlayerFitWalkReport();
|
OutReport = FVoxelPlayerFitWalkReport();
|
||||||
|
if (OutSampleGrid != nullptr)
|
||||||
|
{
|
||||||
|
*OutSampleGrid = FVoxelStrateSampleGrid();
|
||||||
|
}
|
||||||
OutReport.CapsuleRadiusMeters = Settings.PlayerCapsuleRadiusVoxels
|
OutReport.CapsuleRadiusMeters = Settings.PlayerCapsuleRadiusVoxels
|
||||||
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
* FVoxelPlayerCapsuleConstants::VoxelSizeMeters;
|
||||||
OutReport.CapsuleWidthMeters = 2.0f * OutReport.CapsuleRadiusMeters;
|
OutReport.CapsuleWidthMeters = 2.0f * OutReport.CapsuleRadiusMeters;
|
||||||
@@ -3271,6 +3358,13 @@ bool VF_MeasurePlayerFitWalkWithSampler(
|
|||||||
{
|
{
|
||||||
OutReport.RefusalReason = Diagnostics.RefusalReason;
|
OutReport.RefusalReason = Diagnostics.RefusalReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (OutSampleGrid != nullptr)
|
||||||
|
{
|
||||||
|
ExportSampleGrid(Grid, *OutSampleGrid);
|
||||||
|
OutSampleGrid->PlayerFitMask = MoveTemp(PlayerFitMask);
|
||||||
|
OutSampleGrid->PlayerFitCellCount = NumPlayerFitCells;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ struct VOXELFORGE_API FVoxelStrateSampleGrid
|
|||||||
// empty on legacy/binary-only callers. This is never used by generation.
|
// empty on legacy/binary-only callers. This is never used by generation.
|
||||||
TArray<float> Density;
|
TArray<float> Density;
|
||||||
|
|
||||||
|
// 1 = the exact player-fit pose predicate accepted this cell, 0 = rejected. This is
|
||||||
|
// populated when a player-fit walk explicitly captures its shared grid. It is kept beside
|
||||||
|
// Air so render, walk, and diagnostics can consume one sampled field and one fit mask.
|
||||||
|
TArray<uint8> PlayerFitMask;
|
||||||
|
int64 PlayerFitCellCount = 0;
|
||||||
|
|
||||||
FORCEINLINE int32 Index(int32 X, int32 Y, int32 Z) const
|
FORCEINLINE int32 Index(int32 X, int32 Y, int32 Z) const
|
||||||
{
|
{
|
||||||
return static_cast<int32>(
|
return static_cast<int32>(
|
||||||
@@ -163,6 +169,19 @@ struct VOXELFORGE_API FVoxelStrateSampleGrid
|
|||||||
{
|
{
|
||||||
return IsValid() && Density.Num() == CellCount;
|
return IsValid() && Density.Num() == CellCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HasPlayerFitMask() const
|
||||||
|
{
|
||||||
|
return IsValid() && PlayerFitMask.Num() == CellCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContainsPoint(const FVector& Point) const
|
||||||
|
{
|
||||||
|
return IsValid()
|
||||||
|
&& Point.X >= MinX && Point.X < MaxX
|
||||||
|
&& Point.Y >= MinY && Point.Y < MaxY
|
||||||
|
&& Point.Z >= MinZ && Point.Z < MaxZ;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Metrics derived from one coarse grid and its single air flood fill. */
|
/** Metrics derived from one coarse grid and its single air flood fill. */
|
||||||
@@ -510,4 +529,5 @@ VOXELFORGE_API bool VF_MeasurePlayerFitWalkWithSampler(
|
|||||||
const FVector& BVoxel,
|
const FVector& BVoxel,
|
||||||
const FVoxelStrateMeasureSettings& Settings,
|
const FVoxelStrateMeasureSettings& Settings,
|
||||||
FVoxelPlayerFitWalkReport& OutReport,
|
FVoxelPlayerFitWalkReport& OutReport,
|
||||||
const TArray<FVector>* ComponentProbePoints = nullptr);
|
const TArray<FVector>* ComponentProbePoints = nullptr,
|
||||||
|
FVoxelStrateSampleGrid* OutSampleGrid = nullptr);
|
||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user