Compare commits
10
Commits
30253f0e89
...
b4ac9969f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4ac9969f5 | ||
|
|
fb052716c6 | ||
|
|
de05e293aa | ||
|
|
6ee433efa5 | ||
|
|
a49725d365 | ||
|
|
85a803b873 | ||
|
|
7486c299d4 | ||
|
|
f36bc1f9b2 | ||
|
|
370c39e473 | ||
|
|
ea3f0d9a52 |
No files matched your search
+11
-5
@@ -114,7 +114,7 @@ remains `0`, and lateral regions remain gated off.
|
||||
|------|------|
|
||||
| `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. |
|
||||
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
|
||||
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|parity`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, and `-Label`; stages the host/assets, isolates user/DDC/config state, serializes Unreal launches, and emits compact `result.json`/`summary.txt` plus full logs. |
|
||||
| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|parity|horizon|horizon-render|determinism`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, and `-SharedDDC`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, and emits compact `result.json`/`summary.txt` plus full logs. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`. |
|
||||
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
|
||||
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. |
|
||||
| `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. |
|
||||
@@ -260,7 +260,7 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
||||
| Group | Fields (line) |
|
||||
|-------|---------------|
|
||||
| Streaming | `ViewDistanceXY=16`, `ViewDistanceUp/Down=5`, `MaxConcurrentTasks=16`, `MaxMeshAppliesPerFrame=4` (defaults — actual values live on the data asset) |
|
||||
| Clipmap | `ClipRadius`, `MaxClipLevel`, `FullResClipLevels`, `CoarseTileCells`, `RenderDistanceChunks` (custom horizontal reach: the outermost shell keeps generating until it covers this many chunks; 0 = off), `bFarSheetRing` + `FarSheetSpanLevels` (F18 — the render-distance ring streams per-surface SHEETS: level MaxClipLevel+span heightfield tiles instead of MC, see `GenerateSheetMesh`), skirts, `LODOctaveDrop` (T2.b octave drop on coarse tiles — 0 = off/byte-identical) (the old `LOD0/1Distance` + `ContentMaxLevel` were dead → removed) |
|
||||
| Clipmap | `ClipRadius`, `MaxClipLevel=5`, `FullResClipLevels`, `CoarseTileCells`, `RenderDistanceChunks=768` (measured ship defaults; custom horizontal reach: the outermost shell keeps generating until it covers this many chunks; 0 = off), `bFarSheetRing=false` by default, plus `FarSheetSpanLevels` (F18 — the optional render-distance ring streams per-surface SHEETS: level MaxClipLevel+span heightfield tiles instead of MC, see `GenerateSheetMesh`), skirts, `LODOctaveDrop` (T2.b octave drop on coarse tiles — 0 = off/byte-identical). Existing serialized data assets retain their authored values until deliberately migrated. |
|
||||
| World bounds | `WorldRadiusVoxels=8192` and `EdgeSealThickness=64` (actor-space XY radial shell; radius 0 = true legacy no-op) |
|
||||
| Lighting | `bEnableDensityVolume` + DensityVolume* tunables (§3.11 density clipmap / mini-sun shadows) |
|
||||
| Rendering | `VoxelMaterial` (61) |
|
||||
@@ -270,7 +270,7 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
||||
### 3.5 World orchestrator — `Public/VoxelWorld.h` + `Private/VoxelWorld.cpp`
|
||||
`AVoxelWorld : AActor` — owns everything, drives streaming. Also `FChunkResult` struct
|
||||
(VoxelWorld.h:35) = async task payload (coord, chunk, meshdata, LOD, **Epoch**, request timing,
|
||||
desired-set epoch and obsolete/cancellation state).
|
||||
worker-built stream geometry counts/bytes, desired-set epoch and obsolete/cancellation state).
|
||||
|
||||
**Owned objects (UPROPERTY):** `Settings`, `Generator`, `Mesher`, `StrateManager`,
|
||||
`DiffLayer` (VoxelWorld.h:55-78). **Storage:** `Chunks` map, `ChunkMeshes` map,
|
||||
@@ -307,11 +307,15 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
||||
| `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale generation epochs and keys no longer desired, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). |
|
||||
| `FVoxelStackSampler` / `FScopedVoxelStackRegistration` (`VoxelStackSampler.h/.cpp`) | — | **Diagnostic raw stack sampler, off by default.** A sampler thread captures registered generation workers' stacks (`CaptureThreadStackBackTrace`) every N µs and tags each with the tile's LOD. It writes bounded raw PCs to CSV plus a symbolization-free summary under `Saved/`; symbol resolution is deferred until after process exit so the measured process never initializes/calls DbgHelp for this diagnostic. Lifecycle teardown is serialized and registration TLS is session-tagged. Registration is per tile TASK in `LoadTile` (and the export's tasks), never per sample; disabled costs one relaxed atomic load. Game `-voxel.SampleStacks=<us>`, commandlet `-samplestacks[=us]`. |
|
||||
| Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **LOD-gated:** fused path at LOD ≥ `voxel.TileCacheWindowMinLOD` (3), op-stack at LOD ≥ `voxel.TileCacheWindowOpStackMinLOD` (4); lower LODs keep the 4-chunk worker window, because the tile window slowed LOD0 evaluation ~6%. Strates whose authored terrain ops exceed the proven 256-voxel extent (or are malformed/unloaded) fall back to the legacy window (`VF_ComputeTerrainOpEnvelope`). `voxel.SpatialIndex` is -1 auto (off at fused LOD0, on elsewhere), 0 or 1. **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). |
|
||||
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): [ClassifyTile (T1.d) + exact validation, only when `voxel.OuterClassifierMode=1`; **default 0 since 2026-09-13**] → `GenerateMesh`/`GenerateSheetMesh` → `BuildTileStreamSet`. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
|
||||
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): [ClassifyTile (T1.d) + exact validation, only when `voxel.OuterClassifierMode=1`; **default 0 since 2026-09-13**] → `GenerateMesh`/`GenerateSheetMesh` → `BuildTileStreamSet`; records vertex count and resident stream allocation bytes for the horizon trace. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
|
||||
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. |
|
||||
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. |
|
||||
| `UpdateChunksAroundPosition` | 362 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance — as level-MaxClipLevel MC tiles, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. |
|
||||
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. |
|
||||
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The `GetDensityAt` column at capsule feet defines the local air/solid surface; each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; support submission/readiness, travel, and holds are diagnostics. Result logs include the terminal density column and the first crossing below spawn height. |
|
||||
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants) for the reconcile pass. |
|
||||
| `VoxelClipmapDesiredTiles::Build` / `AlignedRingBounds` (`Private/VoxelClipmapDesiredTiles.h`) | — | The pure clipmap selector. **Every finer ring snaps outward to a child-pair boundary** (`AlignedRingBounds`, alignment 2; the last MC ring aligns to the sheet grouping width), so a coarser tile is wholly inside or wholly outside the finer ring: **no two desired tiles of different levels overlap** (a 7-wide odd ring used to leave one half-covered coarse tile per axis side, drawn over the fine one: 635 pairs at the owner config). Cost: LOD0 ring 343 → 512 tiles. Guarded by `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage`. Anchor tiles appended later are outside this guarantee. |
|
||||
| `ReconcileReadyTransitionVisibility` | — | Visibility-only pass while streaming is unsettled: hides a retired tile once all desired tiles over its footprint are loaded, reveals new tiles once no visible retired tile overlaps them. **Indexed** (dyadic ancestors + `DesiredTransitionDescendants`, visible retired tiles collected once per pass): moving p95 32 ms → 0.31 ms. `voxel.MeasureReadyTransitionVisibility=1` logs per-frame samples. |
|
||||
| `RequestCollisionGateSupportTile` / `UpdatePawnCollisionGate` | — | The gate submits the pawn's level-0 support tile **immediately** as `CollisionCritical`, even before the first desired set exists (the desired rebuild then adopts the same pending key). The gate holds downward Z only, with **no timeout** (the old 2 s release fired at every startup, while the game thread applied nothing for ~2.2 s). |
|
||||
| `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set (deduped by stamp) so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. |
|
||||
| `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. |
|
||||
| `RegisterStreamingAnchor` / `UnregisterStreamingAnchor` | — | **BlueprintCallable §9.3:** add/remove an actor as a streaming anchor (`EVoxelAnchorPolicy` CollisionOnly/FullVisual + XY/ZBelow/ZAbove box). Idempotent; forces a rebuild next Tick. §9.4: CollisionOnly tiles cook collision but are hidden (no draw/VSM) unless the player clipmap wants them too. |
|
||||
@@ -737,6 +741,8 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
||||
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries, then audits all inter-strate landing ends: body-derived room dimensions, final-density flat-floor slope, local floor/join geometry, seal containment, hot-call/rebuild timing, and targeted `ClassifyTile` box soundness. Reports source-fit/refused mouths and per-archetype landing slopes; fails on any floor, join, seal, or box violation. |
|
||||
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The shaft seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts. |
|
||||
| `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes after warming six different neighbouring chunk contexts in both legacy and operator-stack paths. Latest result: **0 legacy mismatches, 0 operator mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. |
|
||||
| `VoxelForgeClipmapNoOverlapTest.cpp` | `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage` | Sweeps 6,174 centres (negative coords, every parity) × radius/level/render-distance/vertical-clamp/sheet combinations: **zero overlapping desired pairs AND coverage ⊇ the frozen `7486c29` selector**. Failed on `7486c29` (1,022,679 overlapping pairs) as intended. |
|
||||
| `VoxelForgeDensityBlockEquivalenceTest.cpp` | `VoxelForge.Density.OperatorBlockMatchesScalarGrid` | Block evaluation (`EvalBlock` after `BuildActiveOpList` pruning) vs scalar, **bit-identical**: 5.1M mesher-grid samples (6 seeds × 8 archetypes × levels 0–5, with edits) + 300k direct `EvalBlock` samples. Scopes `voxel.UseFusedEvaluator=0` so the real block path runs. An injected optimistic `FPassageCarveOp::EffectOverBox` produced 5,612 mismatches. |
|
||||
| `VoxelForgeWorldEdgeSealTest.cpp` | `VoxelForge.Generation.WorldEdgeSeal` | Positive samples outside the radius across every fixture strate, bit-identical interior negative control against radius 0, monotonic smooth-ramp check, radius-0 no-op, radial `ClassifyBox`/T1.d proofs brute-forced for every reported voxel, and synthetic plus generated near-rim passage coverage. Emits one aggregate summary with proved-box and voxel counts. |
|
||||
|
||||
## 4. The density pipeline (most-edited hot path)
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "VoxelClipmapDesiredTiles.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using VoxelClipmapDesiredTiles::FParameters;
|
||||
|
||||
struct FOverlapMeasure
|
||||
{
|
||||
uint64 PairCount = 0;
|
||||
uint64 PairwiseVolumeChunkCubed = 0;
|
||||
};
|
||||
|
||||
static FOverlapMeasure MeasureOverlaps(const TArray<FVoxelTileKey>& Tiles)
|
||||
{
|
||||
TSet<FVoxelTileKey> TileSet;
|
||||
int32 MaxLevel = 0;
|
||||
for (const FVoxelTileKey& Tile : Tiles)
|
||||
{
|
||||
TileSet.Add(Tile);
|
||||
MaxLevel = FMath::Max(MaxLevel, Tile.Level);
|
||||
}
|
||||
|
||||
FOverlapMeasure Result;
|
||||
for (const FVoxelTileKey& Fine : Tiles)
|
||||
{
|
||||
for (int32 CoarseLevel = Fine.Level + 1; CoarseLevel <= MaxLevel; ++CoarseLevel)
|
||||
{
|
||||
const int32 Divisor = 1 << (CoarseLevel - Fine.Level);
|
||||
const FVoxelTileKey Coarse(
|
||||
VoxelClipmapDesiredTiles::FloorDiv(Fine.Coord, Divisor), CoarseLevel);
|
||||
if (TileSet.Contains(Coarse))
|
||||
{
|
||||
++Result.PairCount;
|
||||
Result.PairwiseVolumeChunkCubed += (uint64(1) << (3 * Fine.Level));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
static bool IsFootprintCovered(const FVoxelTileKey& Tile,
|
||||
const TSet<FVoxelTileKey>& CoveringTiles,
|
||||
int32 MaxLevel, int32 MinChunkZ, int32 MaxChunkZ)
|
||||
{
|
||||
const int32 TileMinZ = Tile.Coord.Z * (1 << Tile.Level);
|
||||
const int32 TileMaxZ = (Tile.Coord.Z + 1) * (1 << Tile.Level) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (CoveringTiles.Contains(Tile))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// A coarser dyadic tile covers this whole footprint when it is an ancestor.
|
||||
for (int32 ParentLevel = Tile.Level + 1; ParentLevel <= MaxLevel; ++ParentLevel)
|
||||
{
|
||||
const int32 Divisor = 1 << (ParentLevel - Tile.Level);
|
||||
const FVoxelTileKey Parent(
|
||||
VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord, Divisor), ParentLevel);
|
||||
if (CoveringTiles.Contains(Parent))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Tile.Level == 0)
|
||||
{
|
||||
return Tile.Coord.Z < MinChunkZ || Tile.Coord.Z > MaxChunkZ;
|
||||
}
|
||||
|
||||
const int32 ChildLevel = Tile.Level - 1;
|
||||
const FIntVector ChildBase = Tile.Coord * 2;
|
||||
for (int32 Z = 0; Z < 2; ++Z)
|
||||
for (int32 Y = 0; Y < 2; ++Y)
|
||||
for (int32 X = 0; X < 2; ++X)
|
||||
{
|
||||
if (!IsFootprintCovered(
|
||||
FVoxelTileKey(ChildBase + FIntVector(X, Y, Z), ChildLevel),
|
||||
CoveringTiles, MaxLevel, MinChunkZ, MaxChunkZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool CoversEveryLegacyTile(const TArray<FVoxelTileKey>& Legacy,
|
||||
const TArray<FVoxelTileKey>& Current,
|
||||
int32 MinChunkZ, int32 MaxChunkZ)
|
||||
{
|
||||
TSet<FVoxelTileKey> CurrentSet;
|
||||
int32 MaxLevel = 0;
|
||||
for (const FVoxelTileKey& Tile : Current)
|
||||
{
|
||||
CurrentSet.Add(Tile);
|
||||
MaxLevel = FMath::Max(MaxLevel, Tile.Level);
|
||||
}
|
||||
for (const FVoxelTileKey& Tile : Legacy)
|
||||
{
|
||||
if (!IsFootprintCovered(Tile, CurrentSet, MaxLevel, MinChunkZ, MaxChunkZ))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Frozen copy of the 7486c29 selector. It is the coverage oracle and keeps the regression
|
||||
// test able to prove that an exact tiling still covers every volume the old selector covered.
|
||||
static void Build7486c29(const FIntVector& Center, int32 MinChunkZ, int32 MaxChunkZ,
|
||||
const FParameters& Parameters, TArray<FVoxelTileKey>& OutTiles)
|
||||
{
|
||||
OutTiles.Reset();
|
||||
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
|
||||
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
|
||||
const int32 OuterRadius = VoxelClipmapDesiredTiles::OuterShellRadius(
|
||||
Parameters, Radius, MaxLevel);
|
||||
int32 SheetLevel = MaxLevel;
|
||||
int32 SheetRadius = OuterRadius;
|
||||
VoxelClipmapDesiredTiles::OuterShell(
|
||||
Parameters, Radius, MaxLevel, SheetLevel, SheetRadius);
|
||||
const bool bSheetRing = SheetLevel > MaxLevel;
|
||||
const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel;
|
||||
|
||||
for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level)
|
||||
{
|
||||
const int32 Pow = 1 << Level;
|
||||
const FIntVector CenterLevel = VoxelClipmapDesiredTiles::FloorDiv(Center, Pow);
|
||||
const FIntVector CenterFiner = Level > 0
|
||||
? VoxelClipmapDesiredTiles::FloorDiv(Center, Pow >> 1)
|
||||
: FIntVector::ZeroValue;
|
||||
const int32 LevelRadius = (Level == MaxLevel && !bSheetRing)
|
||||
? OuterRadius : Radius;
|
||||
|
||||
int32 DeltaZMin = -LevelRadius;
|
||||
int32 DeltaZMax = LevelRadius;
|
||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||
{
|
||||
DeltaZMin = FMath::Max(
|
||||
DeltaZMin, VoxelClipmapDesiredTiles::FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
||||
DeltaZMax = FMath::Min(
|
||||
DeltaZMax, VoxelClipmapDesiredTiles::FloorDiv(MaxChunkZ, Pow) - CenterLevel.Z);
|
||||
}
|
||||
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = -LevelRadius; DeltaY <= LevelRadius; ++DeltaY)
|
||||
for (int32 DeltaX = -LevelRadius; DeltaX <= LevelRadius; ++DeltaX)
|
||||
{
|
||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << Level;
|
||||
const int32 TileMaxZ = ((Tile.Z + 1) << Level) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Level > 0)
|
||||
{
|
||||
const bool bCovered =
|
||||
(2 * Tile.X >= CenterFiner.X - Radius)
|
||||
&& (2 * Tile.X + 1 <= CenterFiner.X + Radius)
|
||||
&& (2 * Tile.Y >= CenterFiner.Y - Radius)
|
||||
&& (2 * Tile.Y + 1 <= CenterFiner.Y + Radius)
|
||||
&& (2 * Tile.Z >= CenterFiner.Z - Radius)
|
||||
&& (2 * Tile.Z + 1 <= CenterFiner.Z + Radius);
|
||||
if (bCovered)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
OutTiles.Emplace(Tile, Level);
|
||||
}
|
||||
}
|
||||
|
||||
if (bSheetRing)
|
||||
{
|
||||
const int32 SheetPow = 1 << SheetLevel;
|
||||
const FIntVector CenterSheet = VoxelClipmapDesiredTiles::FloorDiv(Center, SheetPow);
|
||||
const FIntVector CenterMC = VoxelClipmapDesiredTiles::FloorDiv(
|
||||
Center, 1 << MaxMCRingLevel);
|
||||
const int32 LevelDelta = SheetLevel - MaxMCRingLevel;
|
||||
|
||||
int32 DeltaZMin = -SheetRadius;
|
||||
int32 DeltaZMax = SheetRadius;
|
||||
if (MinChunkZ != MIN_int32)
|
||||
{
|
||||
DeltaZMin = FMath::Max(DeltaZMin,
|
||||
VoxelClipmapDesiredTiles::FloorDiv(MinChunkZ, SheetPow) - CenterSheet.Z);
|
||||
DeltaZMax = FMath::Min(DeltaZMax,
|
||||
VoxelClipmapDesiredTiles::FloorDiv(MaxChunkZ, SheetPow) - CenterSheet.Z);
|
||||
}
|
||||
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = -SheetRadius; DeltaY <= SheetRadius; ++DeltaY)
|
||||
for (int32 DeltaX = -SheetRadius; DeltaX <= SheetRadius; ++DeltaX)
|
||||
{
|
||||
const FIntVector Tile = CenterSheet + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << SheetLevel;
|
||||
const int32 TileMaxZ = ((Tile.Z + 1) << SheetLevel) - 1;
|
||||
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool bCovered =
|
||||
((Tile.X << LevelDelta) >= CenterMC.X - Radius)
|
||||
&& ((((Tile.X + 1) << LevelDelta) - 1) <= CenterMC.X + Radius)
|
||||
&& ((Tile.Y << LevelDelta) >= CenterMC.Y - Radius)
|
||||
&& ((((Tile.Y + 1) << LevelDelta) - 1) <= CenterMC.Y + Radius)
|
||||
&& ((Tile.Z << LevelDelta) >= CenterMC.Z - Radius)
|
||||
&& ((((Tile.Z + 1) << LevelDelta) - 1) <= CenterMC.Z + Radius);
|
||||
if (!bCovered)
|
||||
{
|
||||
OutTiles.Emplace(Tile, SheetLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static FString DescribeMeasure(const FOverlapMeasure& Measure)
|
||||
{
|
||||
return FString::Printf(TEXT("pairs=%llu pairwise_volume_chunk3=%llu"),
|
||||
static_cast<unsigned long long>(Measure.PairCount),
|
||||
static_cast<unsigned long long>(Measure.PairwiseVolumeChunkCubed));
|
||||
}
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeClipmapNoOverlapTest,
|
||||
"VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
|
||||
FParameters Owner;
|
||||
Owner.ClipRadius = 3;
|
||||
Owner.MaxClipLevel = 5;
|
||||
Owner.RenderDistanceChunks = 768;
|
||||
Owner.bFarSheetRing = false;
|
||||
|
||||
FParameters Defaults;
|
||||
|
||||
TArray<FVoxelTileKey> LegacyOwner, CurrentOwner, LegacyDefaults, CurrentDefaults;
|
||||
Build7486c29(FIntVector::ZeroValue, MIN_int32, MAX_int32, Owner, LegacyOwner);
|
||||
VoxelClipmapDesiredTiles::Build(
|
||||
FIntVector::ZeroValue, MIN_int32, MAX_int32, Owner, CurrentOwner);
|
||||
Build7486c29(FIntVector::ZeroValue, MIN_int32, MAX_int32, Defaults, LegacyDefaults);
|
||||
VoxelClipmapDesiredTiles::Build(
|
||||
FIntVector::ZeroValue, MIN_int32, MAX_int32, Defaults, CurrentDefaults);
|
||||
|
||||
const FOverlapMeasure LegacyOwnerOverlap = MeasureOverlaps(LegacyOwner);
|
||||
const FOverlapMeasure CurrentOwnerOverlap = MeasureOverlaps(CurrentOwner);
|
||||
const FOverlapMeasure LegacyDefaultOverlap = MeasureOverlaps(LegacyDefaults);
|
||||
const FOverlapMeasure CurrentDefaultOverlap = MeasureOverlaps(CurrentDefaults);
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("owner center=(0,0,0): before tiles=%d %s; after tiles=%d %s"),
|
||||
LegacyOwner.Num(), *DescribeMeasure(LegacyOwnerOverlap),
|
||||
CurrentOwner.Num(), *DescribeMeasure(CurrentOwnerOverlap)));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("FParameters defaults center=(0,0,0): before tiles=%d %s; after tiles=%d %s"),
|
||||
LegacyDefaults.Num(), *DescribeMeasure(LegacyDefaultOverlap),
|
||||
CurrentDefaults.Num(), *DescribeMeasure(CurrentDefaultOverlap)));
|
||||
|
||||
TestTrue(TEXT("7486c29 owner overlap measurement matches the checked source geometry"),
|
||||
LegacyOwnerOverlap.PairCount == 635
|
||||
&& LegacyOwnerOverlap.PairwiseVolumeChunkCubed == 594487);
|
||||
TestTrue(TEXT("7486c29 FParameters-default overlap measurement matches the checked source geometry"),
|
||||
LegacyDefaultOverlap.PairCount == 508
|
||||
&& LegacyDefaultOverlap.PairwiseVolumeChunkCubed == 74295);
|
||||
TestTrue(TEXT("owner desired set has no overlapping level pairs"),
|
||||
CurrentOwnerOverlap.PairCount == 0 && CurrentOwnerOverlap.PairwiseVolumeChunkCubed == 0);
|
||||
TestTrue(TEXT("default desired set has no overlapping level pairs"),
|
||||
CurrentDefaultOverlap.PairCount == 0 && CurrentDefaultOverlap.PairwiseVolumeChunkCubed == 0);
|
||||
TestTrue(TEXT("owner coverage is identical or larger than 7486c29"),
|
||||
CoversEveryLegacyTile(LegacyOwner, CurrentOwner, MIN_int32, MAX_int32));
|
||||
TestTrue(TEXT("default coverage is identical or larger than 7486c29"),
|
||||
CoversEveryLegacyTile(LegacyDefaults, CurrentDefaults, MIN_int32, MAX_int32));
|
||||
|
||||
struct FSweepSettings
|
||||
{
|
||||
int32 Radius;
|
||||
int32 MaxLevel;
|
||||
int32 RenderDistance;
|
||||
bool bSheet;
|
||||
int32 SheetSpan;
|
||||
};
|
||||
static const FSweepSettings SweepSettings[] = {
|
||||
{1, 0, 0, false, 2},
|
||||
{2, 2, 0, false, 2},
|
||||
{3, 4, 0, false, 2},
|
||||
{2, 3, 49, false, 2},
|
||||
{2, 3, 49, true, 2},
|
||||
{2, 4, 87, true, 3},
|
||||
};
|
||||
|
||||
int32 SweptCases = 0;
|
||||
int32 OverlapCases = 0;
|
||||
int32 CoverageFailures = 0;
|
||||
uint64 TotalOverlapPairs = 0;
|
||||
FString FirstFailure;
|
||||
const int32 CenterMin = -3;
|
||||
const int32 CenterMax = 3;
|
||||
for (int32 Z = CenterMin; Z <= CenterMax; ++Z)
|
||||
for (int32 Y = CenterMin; Y <= CenterMax; ++Y)
|
||||
for (int32 X = CenterMin; X <= CenterMax; ++X)
|
||||
for (const FSweepSettings& Sweep : SweepSettings)
|
||||
{
|
||||
FParameters P;
|
||||
P.ClipRadius = Sweep.Radius;
|
||||
P.MaxClipLevel = Sweep.MaxLevel;
|
||||
P.RenderDistanceChunks = Sweep.RenderDistance;
|
||||
P.bFarSheetRing = Sweep.bSheet;
|
||||
P.FarSheetSpanLevels = Sweep.SheetSpan;
|
||||
|
||||
// Exercise the unbounded view, a tight local clamp, and a clamp crossing a tile boundary.
|
||||
for (int32 ClampCase = 0; ClampCase < 3; ++ClampCase)
|
||||
{
|
||||
int32 MinZ = MIN_int32;
|
||||
int32 MaxZ = MAX_int32;
|
||||
if (ClampCase == 1)
|
||||
{
|
||||
MinZ = Z - 2;
|
||||
MaxZ = Z + 3;
|
||||
}
|
||||
else if (ClampCase == 2)
|
||||
{
|
||||
const int32 Band = VoxelClipmapDesiredTiles::FloorDiv(Z, 1 << Sweep.MaxLevel);
|
||||
MinZ = (Band << Sweep.MaxLevel) - 1;
|
||||
MaxZ = MinZ + 6;
|
||||
}
|
||||
|
||||
const FIntVector Center(X, Y, Z);
|
||||
TArray<FVoxelTileKey> Legacy, Current;
|
||||
Build7486c29(Center, MinZ, MaxZ, P, Legacy);
|
||||
VoxelClipmapDesiredTiles::Build(Center, MinZ, MaxZ, P, Current);
|
||||
++SweptCases;
|
||||
|
||||
const FOverlapMeasure CurrentOverlap = MeasureOverlaps(Current);
|
||||
if (CurrentOverlap.PairCount != 0)
|
||||
{
|
||||
++OverlapCases;
|
||||
TotalOverlapPairs += CurrentOverlap.PairCount;
|
||||
if (FirstFailure.IsEmpty())
|
||||
{
|
||||
FirstFailure = FString::Printf(
|
||||
TEXT("overlap center=(%d,%d,%d) radius=%d level=%d distance=%d sheet=%d clamp=%d: %s"),
|
||||
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
|
||||
Sweep.bSheet ? 1 : 0, ClampCase, *DescribeMeasure(CurrentOverlap));
|
||||
}
|
||||
}
|
||||
if (!CoversEveryLegacyTile(Legacy, Current, MinZ, MaxZ))
|
||||
{
|
||||
++CoverageFailures;
|
||||
if (FirstFailure.IsEmpty())
|
||||
{
|
||||
FirstFailure = FString::Printf(
|
||||
TEXT("coverage center=(%d,%d,%d) radius=%d level=%d distance=%d sheet=%d clamp=%d"),
|
||||
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
|
||||
Sweep.bSheet ? 1 : 0, ClampCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("sweep cases=%d centers=343 settings=%d clamps=3 overlap_cases=%d overlap_pairs=%llu coverage_failures=%d first=%s"),
|
||||
SweptCases, UE_ARRAY_COUNT(SweepSettings), OverlapCases,
|
||||
static_cast<unsigned long long>(TotalOverlapPairs), CoverageFailures,
|
||||
FirstFailure.IsEmpty() ? TEXT("none") : *FirstFailure));
|
||||
TestEqual(TEXT("all swept desired sets have zero overlapping pairs"), OverlapCases, 0);
|
||||
TestEqual(TEXT("all swept desired sets cover the legacy footprint"), CoverageFailures, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -238,8 +239,11 @@ namespace
|
||||
int32 AllAir = 0;
|
||||
int32 Proved = 0;
|
||||
int64 CheckedVoxels = 0;
|
||||
int64 ExactIntervalChecks = 0;
|
||||
int64 ExactIntervalDisagreements = 0;
|
||||
int32 Violations = 0;
|
||||
FString FirstViolation;
|
||||
TArray<FString> ViolationDetails;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -248,7 +252,8 @@ namespace
|
||||
* to expose. The report is aggregate: there is no per-sample logging.
|
||||
*/
|
||||
FBoxVerdictReport VF_CheckRolledBoxVerdicts(
|
||||
const VoxelForgeTest::FTestWorld& World, int32 CandidateIndex, int32 StrateIndex)
|
||||
const VoxelForgeTest::FTestWorld& World, int32 CandidateIndex, int32 StrateIndex,
|
||||
const FVoxelStrateRollInfo& Roll)
|
||||
{
|
||||
FBoxVerdictReport Report;
|
||||
const UVoxelGenerator* Generator = World.Generator.Get();
|
||||
@@ -286,8 +291,18 @@ namespace
|
||||
(float)(Origin.Y + (Cells + 1) * Step),
|
||||
(float)(Origin.Z + (Cells + 1) * Step)));
|
||||
(void)Box; // The production API receives the equivalent origin/step/cell box.
|
||||
const FBox ClassifierBox(
|
||||
FVector(static_cast<float>(Origin.X), static_cast<float>(Origin.Y),
|
||||
static_cast<float>(Origin.Z)),
|
||||
FVector(static_cast<float>(Origin.X + Cells * Step),
|
||||
static_cast<float>(Origin.Y + Cells * Step),
|
||||
static_cast<float>(Origin.Z + Cells * Step)));
|
||||
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(Origin, Step, Cells);
|
||||
FVoxelTileClassificationStats ClassifyStats;
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(
|
||||
Origin, Step, Cells, &ClassifyStats);
|
||||
Report.ExactIntervalChecks += ClassifyStats.ExactCoreIntervalChecks;
|
||||
Report.ExactIntervalDisagreements += ClassifyStats.ExactCoreIntervalDisagreements;
|
||||
if (Verdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
++Report.Mixed;
|
||||
@@ -305,6 +320,15 @@ namespace
|
||||
|
||||
const bool bClaimsSolid = Verdict == EVoxelTileClass::AllSolid;
|
||||
bool bBoxBad = false;
|
||||
bool bDiagnosticStackReady = false;
|
||||
bool bDiagnosticStackBuilt = false;
|
||||
FVoxelOpStack DiagnosticStack;
|
||||
FVoxelOpContext DiagnosticContext;
|
||||
EVoxelTileClass DiagnosticStackVerdict = EVoxelTileClass::Mixed;
|
||||
int32 DiagnosticSolidKiller = INDEX_NONE;
|
||||
int32 DiagnosticAirKiller = INDEX_NONE;
|
||||
TArray<FVoxelOpStack::FOpBoxDiagnostic> OpDiagnostics;
|
||||
FString WriterRouteDetails;
|
||||
// ClassifyTile covers the cell vertices passed to it: g is [0, Cells]. The mesher
|
||||
// samples a separate halo, but that halo is not part of this production verdict's
|
||||
// proof domain and must not turn a sound tile into a false failure.
|
||||
@@ -325,6 +349,173 @@ namespace
|
||||
{
|
||||
++Report.Violations;
|
||||
bBoxBad = true;
|
||||
if (!bDiagnosticStackReady)
|
||||
{
|
||||
bDiagnosticStackReady = true;
|
||||
int32 SlotTop = 0, SlotBottom = 0;
|
||||
FVoxelStrateArchetypeParams RuntimeParams = Roll.ArchetypeParams;
|
||||
bDiagnosticStackBuilt = World.GetSlotVoxelZRange(
|
||||
StrateIndex, SlotTop, SlotBottom);
|
||||
if (bDiagnosticStackBuilt)
|
||||
{
|
||||
VF_SetStrateArchetypeRuntimeBounds(
|
||||
RuntimeParams, static_cast<float>(SlotTop) + 1.0f,
|
||||
static_cast<float>(SlotBottom));
|
||||
bDiagnosticStackBuilt = VF_BuildNativeStrateStackForCandidate(
|
||||
Roll.Archetype, RuntimeParams, World.Settings->Seed,
|
||||
World.Generator->OriginSpineRadius,
|
||||
World.Generator->WorldRadiusVoxels,
|
||||
World.Generator->EdgeSealThickness,
|
||||
World.StrateManager.Get(), DiagnosticStack, DiagnosticContext);
|
||||
}
|
||||
if (bDiagnosticStackBuilt)
|
||||
{
|
||||
DiagnosticContext.ChunkCoord = FIntVector(
|
||||
0, 0, VF_FloorDiv(Origin.Z, CHUNK_SIZE));
|
||||
DiagnosticContext.Step = Step;
|
||||
DiagnosticContext.LayoutVersion =
|
||||
World.StrateManager->GetLayoutVersion();
|
||||
DiagnosticContext.WorldRadiusVoxels =
|
||||
World.Generator->WorldRadiusVoxels;
|
||||
DiagnosticContext.EdgeSealThickness =
|
||||
World.Generator->EdgeSealThickness;
|
||||
DiagnosticContext.bUseLatticeProof = true;
|
||||
DiagnosticContext.bTightenWarpProof = Step <= 2;
|
||||
DiagnosticContext.LatticeOriginVoxels = Origin;
|
||||
DiagnosticStack.PrepareChunk(DiagnosticContext);
|
||||
DiagnosticStackVerdict = DiagnosticStack.ClassifyBoxAttributed(
|
||||
ClassifierBox, DiagnosticContext,
|
||||
DiagnosticSolidKiller, DiagnosticAirKiller);
|
||||
DiagnosticStack.DiagnoseBox(
|
||||
ClassifierBox, DiagnosticContext, Step, OpDiagnostics,
|
||||
&DiagnosticStackVerdict);
|
||||
}
|
||||
}
|
||||
if (WriterRouteDetails.IsEmpty())
|
||||
{
|
||||
const FIntVector SampleChunk(
|
||||
VF_FloorDiv(static_cast<int32>(X), CHUNK_SIZE),
|
||||
VF_FloorDiv(static_cast<int32>(Y), CHUNK_SIZE),
|
||||
VF_FloorDiv(static_cast<int32>(Z), CHUNK_SIZE));
|
||||
int32 SampleSlotTop = 0;
|
||||
int32 SampleSlotBottom = 0;
|
||||
int32 RecipeSeedAtSample = 0;
|
||||
ECaveGeneratorType RecipeArchetypeAtSample = ECaveGeneratorType::TunnelNetwork;
|
||||
FVoxelStrateArchetypeParams RecipeParamsAtSample;
|
||||
FVoxelOpStackRecipe RecipeAtSample;
|
||||
const bool bSampleHasSlot = World.StrateManager->GetStrateChunkZBounds(
|
||||
SampleChunk.Z, SampleSlotTop, SampleSlotBottom);
|
||||
const bool bSampleHasRecipe = World.StrateManager->GetRecipeForChunk(
|
||||
SampleChunk, RecipeSeedAtSample, RecipeArchetypeAtSample,
|
||||
RecipeParamsAtSample, RecipeAtSample);
|
||||
const float PassageSdf = World.StrateManager->EvaluateModifierSDF(X, Y, Z);
|
||||
const float MaxPassageCarve =
|
||||
World.StrateManager->MaxPassageCarveFactorNearLattice(
|
||||
ClassifierBox, Origin, Step);
|
||||
const bool bPassageNear = World.StrateManager->AnyPassageNearLattice(
|
||||
ClassifierBox, Origin, Step);
|
||||
const bool bPassageAirPostNear = World.StrateManager->AnyPassageAirPostNearLattice(
|
||||
ClassifierBox, Origin, Step, 8.0f, World.Generator->EdgeSealThickness);
|
||||
const bool bLandingFloorNear =
|
||||
World.StrateManager->AnyPassageLandingFloorNearLattice(
|
||||
ClassifierBox, Origin, Step);
|
||||
const bool bStructuralPostNear =
|
||||
World.StrateManager->AnyPassageStructuralPostNearLattice(
|
||||
ClassifierBox, Origin, Step);
|
||||
WriterRouteDetails = FString::Printf(
|
||||
TEXT("sample_chunk=(%d,%d,%d) gap=%d slot_bounds=%d:[%d..%d] "
|
||||
"generator=%s operator_stack=%d recipe=%d world_radius=%.6g "
|
||||
"passage_sdf=%.9g passage_max_carve=%.9g "
|
||||
"passage_near=%d passage_air_post=%d landing_floor=%d structural_post=%d"),
|
||||
SampleChunk.X, SampleChunk.Y, SampleChunk.Z,
|
||||
World.StrateManager->IsGapChunk(SampleChunk) ? 1 : 0,
|
||||
bSampleHasSlot ? 1 : 0, SampleSlotTop, SampleSlotBottom,
|
||||
VF_GetStrateArchetypeName(
|
||||
World.StrateManager->GetGeneratorTypeForChunk(SampleChunk)),
|
||||
World.StrateManager->UsesOperatorStackForChunk(SampleChunk) ? 1 : 0,
|
||||
bSampleHasRecipe ? 1 : 0, World.Generator->WorldRadiusVoxels,
|
||||
PassageSdf, MaxPassageCarve, bPassageNear ? 1 : 0,
|
||||
bPassageAirPostNear ? 1 : 0, bLandingFloorNear ? 1 : 0,
|
||||
bStructuralPostNear ? 1 : 0);
|
||||
}
|
||||
float StackDensity = FLT_MAX;
|
||||
FString StackAttribution = TEXT("stack diagnostic unavailable");
|
||||
if (bDiagnosticStackBuilt)
|
||||
{
|
||||
StackDensity = DiagnosticStack.EvalMC(X, Y, Z);
|
||||
FString BoundViolations;
|
||||
FString SdfIntervalViolations;
|
||||
FString OperatorIntervals;
|
||||
for (const FVoxelOpStack::FOpBoxDiagnostic& OpDiagnostic : OpDiagnostics)
|
||||
{
|
||||
if (OpDiagnostic.ForcedVerdict != EVoxelTileClass::Mixed
|
||||
|| OpDiagnostic.bHasDensityDelta
|
||||
|| OpDiagnostic.bHasSdfValue)
|
||||
{
|
||||
if (!OperatorIntervals.IsEmpty()) { OperatorIntervals += TEXT("; "); }
|
||||
OperatorIntervals += FString::Printf(
|
||||
TEXT("%s(forced=%d,effect=%d,carve=%.6g,fill=%.6g,delta=[%.6g,%.6g],sdf_bound=[%.6g,%.6g],sdf_actual=[%.6g,%.6g])"),
|
||||
*OpDiagnostic.Name,
|
||||
static_cast<int32>(OpDiagnostic.ForcedVerdict),
|
||||
static_cast<int32>(OpDiagnostic.Effect),
|
||||
OpDiagnostic.MaxCarve, OpDiagnostic.MaxFill,
|
||||
OpDiagnostic.ActualDensityDeltaMin,
|
||||
OpDiagnostic.ActualDensityDeltaMax,
|
||||
OpDiagnostic.SdfBoundMin, OpDiagnostic.SdfBoundMax,
|
||||
OpDiagnostic.ActualSdfMin, OpDiagnostic.ActualSdfMax);
|
||||
}
|
||||
if (OpDiagnostic.bCarveBoundViolated
|
||||
|| OpDiagnostic.bFillBoundViolated)
|
||||
{
|
||||
if (!BoundViolations.IsEmpty()) { BoundViolations += TEXT(", "); }
|
||||
BoundViolations += FString::Printf(
|
||||
TEXT("%s(carve=%d,fill=%d,delta=[%.6g,%.6g])"),
|
||||
*OpDiagnostic.Name,
|
||||
OpDiagnostic.bCarveBoundViolated ? 1 : 0,
|
||||
OpDiagnostic.bFillBoundViolated ? 1 : 0,
|
||||
OpDiagnostic.ActualDensityDeltaMin,
|
||||
OpDiagnostic.ActualDensityDeltaMax);
|
||||
}
|
||||
if (OpDiagnostic.bHasSdfBound && OpDiagnostic.bHasSdfValue
|
||||
&& (OpDiagnostic.ActualSdfMin < OpDiagnostic.SdfBoundMin - 1.0e-4f
|
||||
|| OpDiagnostic.ActualSdfMax > OpDiagnostic.SdfBoundMax + 1.0e-4f))
|
||||
{
|
||||
if (!SdfIntervalViolations.IsEmpty())
|
||||
{
|
||||
SdfIntervalViolations += TEXT(", ");
|
||||
}
|
||||
SdfIntervalViolations += FString::Printf(
|
||||
TEXT("%s(bound=[%.6g,%.6g],actual=[%.6g,%.6g])"),
|
||||
*OpDiagnostic.Name, OpDiagnostic.SdfBoundMin,
|
||||
OpDiagnostic.SdfBoundMax, OpDiagnostic.ActualSdfMin,
|
||||
OpDiagnostic.ActualSdfMax);
|
||||
}
|
||||
}
|
||||
StackAttribution = FString::Printf(
|
||||
TEXT("stack_verdict=%s solid_killer=%s air_killer=%s stack_density=%.9g "
|
||||
"operator_bound_violations=[%s] sdf_interval_violations=[%s] "
|
||||
"op_intervals=[%s] "
|
||||
"refine_nodes=%u depth=%u exact_core_samples=%u exact_final_samples=%u "
|
||||
"post_nodes=%u"),
|
||||
DiagnosticStackVerdict == EVoxelTileClass::AllSolid ? TEXT("AllSolid")
|
||||
: DiagnosticStackVerdict == EVoxelTileClass::AllAir ? TEXT("AllAir")
|
||||
: TEXT("Mixed"),
|
||||
DiagnosticStack.GetOpDebugName(DiagnosticSolidKiller),
|
||||
DiagnosticStack.GetOpDebugName(DiagnosticAirKiller),
|
||||
StackDensity,
|
||||
BoundViolations.IsEmpty() ? TEXT("none") : *BoundViolations,
|
||||
SdfIntervalViolations.IsEmpty() ? TEXT("none") : *SdfIntervalViolations,
|
||||
OperatorIntervals.IsEmpty() ? TEXT("none") : *OperatorIntervals,
|
||||
ClassifyStats.RefineNodes, ClassifyStats.MaxRefinementDepth,
|
||||
ClassifyStats.ExactCoreSamples, ClassifyStats.ExactFinalSamples,
|
||||
ClassifyStats.NeedsFinalFieldNodes);
|
||||
}
|
||||
Report.ViolationDetails.Add(FString::Printf(
|
||||
TEXT("candidate %d box %d origin (%d,%d,%d) said %s; final density at "
|
||||
"(%0.f,%0.f,%0.f) = %.9g; %s; %s"),
|
||||
CandidateIndex, BoxIndex, Origin.X, Origin.Y, Origin.Z,
|
||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
X, Y, Z, Density, *WriterRouteDetails, *StackAttribution));
|
||||
if (Report.FirstViolation.IsEmpty())
|
||||
{
|
||||
Report.FirstViolation = FString::Printf(
|
||||
@@ -801,6 +992,8 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
int32 TotalBoxAllAir = 0;
|
||||
int32 TotalBoxProved = 0;
|
||||
int64 TotalBoxCheckedVoxels = 0;
|
||||
int64 TotalExactIntervalChecks = 0;
|
||||
int64 TotalExactIntervalDisagreements = 0;
|
||||
int32 TotalBoxViolations = 0;
|
||||
int32 NumTunnelLikeCandidates = 0;
|
||||
int32 NumCandidatesWithLiveDetail = 0;
|
||||
@@ -996,16 +1189,23 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
|
||||
const FBoxVerdictReport BoxReport = VF_CheckRolledBoxVerdicts(
|
||||
World, CandidateIndex, CandidateStrateIndex);
|
||||
World, CandidateIndex, CandidateStrateIndex, Roll);
|
||||
TotalBoxMixed += BoxReport.Mixed;
|
||||
TotalBoxAllSolid += BoxReport.AllSolid;
|
||||
TotalBoxAllAir += BoxReport.AllAir;
|
||||
TotalBoxProved += BoxReport.Proved;
|
||||
TotalBoxCheckedVoxels += BoxReport.CheckedVoxels;
|
||||
TotalExactIntervalChecks += BoxReport.ExactIntervalChecks;
|
||||
TotalExactIntervalDisagreements += BoxReport.ExactIntervalDisagreements;
|
||||
TotalBoxViolations += BoxReport.Violations;
|
||||
if (BoxReport.Violations > 0)
|
||||
{
|
||||
BoxViolationCandidates.Add(CandidateIndex);
|
||||
for (const FString& ViolationDetail : BoxReport.ViolationDetails)
|
||||
{
|
||||
AddInfo(FString::Printf(TEXT("ROLLED BOX-VERDICT COUNTEREXAMPLE: %s"),
|
||||
*ViolationDetail));
|
||||
}
|
||||
if (FirstBoxViolation.IsEmpty())
|
||||
{
|
||||
FirstBoxViolation = BoxReport.FirstViolation;
|
||||
@@ -1098,6 +1298,10 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
TotalBoxMixed, TotalBoxAllSolid, TotalBoxAllAir, TotalBoxProved,
|
||||
TotalBoxCheckedVoxels, TotalBoxViolations,
|
||||
BoxViolationCandidates.Num() == 0 ? TEXT("none") : TEXT("see first violation below")));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Fine room-graph interval checks: %lld exact lattice leaves; %lld interval verdicts "
|
||||
"disagreed with the exact child samples."),
|
||||
TotalExactIntervalChecks, TotalExactIntervalDisagreements));
|
||||
if (!BoxViolationCandidates.IsEmpty())
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
|
||||
@@ -0,0 +1,592 @@
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "VoxelClipmapDesiredTiles.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// The marching-cubes grid contains 32 cells, with one sample of halo on each side.
|
||||
constexpr int32 MesherGridSize = 35;
|
||||
constexpr int32 ArchetypeCount = 8;
|
||||
constexpr int32 PassageGridCount = 2;
|
||||
constexpr int32 GridSetCount = ArchetypeCount + PassageGridCount;
|
||||
|
||||
static bool SameFloatBits(float A, float B)
|
||||
{
|
||||
return FMemory::Memcmp(&A, &B, sizeof(float)) == 0;
|
||||
}
|
||||
|
||||
static FIntVector MakeMesherGridOrigin(const FIntVector& TargetChunk, int32 Level)
|
||||
{
|
||||
const int32 TileSpanChunks = 1 << Level;
|
||||
const FIntVector TileCoord = VoxelClipmapDesiredTiles::FloorDiv(
|
||||
TargetChunk, TileSpanChunks);
|
||||
const int32 Step = 1 << Level;
|
||||
return TileCoord * (CHUNK_SIZE << Level) - FIntVector(Step, Step, Step);
|
||||
}
|
||||
|
||||
struct FGridComparison
|
||||
{
|
||||
int64 Samples = 0;
|
||||
int32 Mismatches = 0;
|
||||
FString FirstMismatch;
|
||||
TArray<float> BlockDensities;
|
||||
};
|
||||
|
||||
static bool BuildProductionStackForArchetype(
|
||||
VoxelForgeTest::FTestWorld& World, ECaveGeneratorType Type,
|
||||
const FIntVector& ChunkCoord, FVoxelOpStack& OutStack, FVoxelOpContext& OutContext)
|
||||
{
|
||||
const int32 Seed = World.Settings->GetEffectiveWorldSeed();
|
||||
const float SpineRadius = World.Settings->GetEffectiveOriginSpineRadius();
|
||||
const UVoxelStrateManager* Manager = World.StrateManager.Get();
|
||||
OutContext = FVoxelOpContext();
|
||||
OutContext.ChunkCoord = ChunkCoord;
|
||||
OutContext.Seed = static_cast<uint32>(Seed);
|
||||
OutContext.LayoutVersion = Manager->GetLayoutVersion();
|
||||
OutContext.WorldRadiusVoxels = World.Settings->GetEffectiveWorldRadiusVoxels();
|
||||
OutContext.EdgeSealThickness = World.Settings->EdgeSealThickness;
|
||||
|
||||
switch (Type)
|
||||
{
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
{
|
||||
const FSlabGenerationParams P = Manager->GetSlabParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildSlabStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::Maze:
|
||||
{
|
||||
const FMazeGenerationParams P = Manager->GetMazeParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildMazeStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
{
|
||||
const FSurfaceGenerationParams P = Manager->GetSurfaceParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
// The generator-grid comparison below covers the resolved biome route. This builder
|
||||
// exercises the production stack's base surface branch directly.
|
||||
VoxelDensityOps::BuildSurfaceStack(
|
||||
OutStack, P, Seed, SpineRadius, Manager, TArray<FSurfaceGenerationParams>(), nullptr);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
{
|
||||
const FVerticalShaftParams P = Manager->GetVerticalShaftParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildVerticalShaftStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
{
|
||||
const FFloatingIslandParams P = Manager->GetFloatingIslandParamsForChunk(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildFloatingIslandStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
{
|
||||
const FStrateGenerationParams P = Manager->GetGenerationParams(ChunkCoord);
|
||||
OutContext.StrateTopWorldZ = P.StrateTopWorldZ;
|
||||
OutContext.StrateBottomWorldZ = P.StrateBottomWorldZ;
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(OutStack, P, Seed, SpineRadius, Manager);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool FindChunkSampleRange(int32 AxisOrigin, int32 Step, int32 AxisSize,
|
||||
int32 TargetChunk, int32& OutMin, int32& OutMax)
|
||||
{
|
||||
OutMin = INT32_MAX;
|
||||
OutMax = INT32_MIN;
|
||||
for (int32 Index = 0; Index < AxisSize; ++Index)
|
||||
{
|
||||
const int32 Coordinate = AxisOrigin + Index * Step;
|
||||
if (FMath::FloorToInt(static_cast<float>(Coordinate) / CHUNK_SIZE) == TargetChunk)
|
||||
{
|
||||
OutMin = FMath::Min(OutMin, Index);
|
||||
OutMax = FMath::Max(OutMax, Index);
|
||||
}
|
||||
}
|
||||
return OutMin != INT32_MAX;
|
||||
}
|
||||
|
||||
static FGridComparison CompareStackChunkBlockWithScalar(
|
||||
FVoxelOpStack& Stack, FIntVector GridOrigin, int32 Step,
|
||||
const FIntVector& TargetChunk)
|
||||
{
|
||||
FGridComparison Result;
|
||||
int32 MinX = 0, MaxX = 0, MinY = 0, MaxY = 0, MinZ = 0, MaxZ = 0;
|
||||
if (!FindChunkSampleRange(GridOrigin.X, Step, MesherGridSize, TargetChunk.X, MinX, MaxX)
|
||||
|| !FindChunkSampleRange(GridOrigin.Y, Step, MesherGridSize, TargetChunk.Y, MinY, MaxY)
|
||||
|| !FindChunkSampleRange(GridOrigin.Z, Step, MesherGridSize, TargetChunk.Z, MinZ, MaxZ))
|
||||
{
|
||||
Result.FirstMismatch = TEXT("mesher grid has no samples in the stack's target chunk");
|
||||
return Result;
|
||||
}
|
||||
|
||||
const int32 SizeX = MaxX - MinX + 1;
|
||||
const int32 SizeY = MaxY - MinY + 1;
|
||||
const int32 SizeZ = MaxZ - MinZ + 1;
|
||||
const FIntVector Origin = GridOrigin + FIntVector(MinX * Step, MinY * Step, MinZ * Step);
|
||||
const FIntVector Last = Origin + FIntVector(
|
||||
(SizeX - 1) * Step, (SizeY - 1) * Step, (SizeZ - 1) * Step);
|
||||
const FBox Box(
|
||||
FVector((float)Origin.X, (float)Origin.Y, (float)Origin.Z),
|
||||
FVector((float)Last.X, (float)Last.Y, (float)Last.Z));
|
||||
|
||||
TArray<int32> ActiveOps;
|
||||
const int32 ActiveCount = Stack.BuildActiveOpList(Box, Step, Origin, ActiveOps);
|
||||
TArray<FVoxelOpSample> Samples;
|
||||
Samples.SetNumUninitialized(SizeX * SizeY * SizeZ);
|
||||
for (FVoxelOpSample& Sample : Samples) { Sample = FVoxelOpSample(); }
|
||||
|
||||
FVoxelOpBlock Block;
|
||||
Block.OriginVoxels = Origin;
|
||||
Block.Step = Step;
|
||||
Block.SizeX = SizeX;
|
||||
Block.SizeY = SizeY;
|
||||
Block.SizeZ = SizeZ;
|
||||
Block.Samples = Samples.GetData();
|
||||
Stack.EvalBlock(Block, ActiveOps);
|
||||
|
||||
for (int32 Z = 0; Z < SizeZ; ++Z)
|
||||
for (int32 Y = 0; Y < SizeY; ++Y)
|
||||
for (int32 X = 0; X < SizeX; ++X)
|
||||
{
|
||||
const FIntVector SamplePos = Origin + FIntVector(X * Step, Y * Step, Z * Step);
|
||||
const float BlockDensity = Block.At(X, Y, Z).Density;
|
||||
const float ScalarDensity = Stack.EvalInternal(
|
||||
(float)SamplePos.X, (float)SamplePos.Y, (float)SamplePos.Z);
|
||||
Result.BlockDensities.Add(BlockDensity);
|
||||
++Result.Samples;
|
||||
if (!SameFloatBits(BlockDensity, ScalarDensity))
|
||||
{
|
||||
++Result.Mismatches;
|
||||
if (Result.FirstMismatch.IsEmpty())
|
||||
{
|
||||
Result.FirstMismatch = FString::Printf(
|
||||
TEXT("box=[(%d,%d,%d)..(%d,%d,%d)] active_ops=%d/%d sample=(%d,%d,%d) block=%.9g scalar=%.9g delta=%.9g"),
|
||||
Origin.X, Origin.Y, Origin.Z, Last.X, Last.Y, Last.Z,
|
||||
ActiveCount, Stack.Num(), SamplePos.X, SamplePos.Y, SamplePos.Z,
|
||||
BlockDensity, ScalarDensity, BlockDensity - ScalarDensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
static FGridComparison CompareGeneratorBlockWithScalar(
|
||||
UVoxelGenerator& Generator, FIntVector Origin, int32 Step)
|
||||
{
|
||||
FGridComparison Result;
|
||||
const int32 Count = MesherGridSize * MesherGridSize * MesherGridSize;
|
||||
Result.BlockDensities.SetNumUninitialized(Count);
|
||||
Generator.BeginDensityBlock(Origin, Step, MesherGridSize, MesherGridSize, MesherGridSize);
|
||||
int32 Index = 0;
|
||||
for (int32 Z = 0; Z < MesherGridSize; ++Z)
|
||||
for (int32 Y = 0; Y < MesherGridSize; ++Y)
|
||||
for (int32 X = 0; X < MesherGridSize; ++X, ++Index)
|
||||
{
|
||||
const FIntVector SamplePos = Origin + FIntVector(X * Step, Y * Step, Z * Step);
|
||||
Result.BlockDensities[Index] = Generator.GetDensityAt(
|
||||
(float)SamplePos.X, (float)SamplePos.Y, (float)SamplePos.Z);
|
||||
}
|
||||
Generator.EndDensityBlock();
|
||||
|
||||
Index = 0;
|
||||
for (int32 Z = 0; Z < MesherGridSize; ++Z)
|
||||
for (int32 Y = 0; Y < MesherGridSize; ++Y)
|
||||
for (int32 X = 0; X < MesherGridSize; ++X, ++Index)
|
||||
{
|
||||
const FIntVector SamplePos = Origin + FIntVector(X * Step, Y * Step, Z * Step);
|
||||
const float ScalarDensity = Generator.GetDensityAt(
|
||||
(float)SamplePos.X, (float)SamplePos.Y, (float)SamplePos.Z);
|
||||
++Result.Samples;
|
||||
if (!SameFloatBits(Result.BlockDensities[Index], ScalarDensity))
|
||||
{
|
||||
++Result.Mismatches;
|
||||
if (Result.FirstMismatch.IsEmpty())
|
||||
{
|
||||
Result.FirstMismatch = FString::Printf(
|
||||
TEXT("sample=(%d,%d,%d) block=%.9g scalar=%.9g delta=%.9g"),
|
||||
SamplePos.X, SamplePos.Y, SamplePos.Z, Result.BlockDensities[Index],
|
||||
ScalarDensity, Result.BlockDensities[Index] - ScalarDensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
static int32 FindGapChunkZ(const VoxelForgeTest::FTestWorld& World)
|
||||
{
|
||||
const int32 Min = World.BottomChunkZ - 8;
|
||||
const int32 Max = World.TopChunkZ + 8;
|
||||
for (int32 ChunkZ = Min; ChunkZ <= Max; ++ChunkZ)
|
||||
{
|
||||
if (World.StrateManager->IsGapChunk(FIntVector(0, 0, ChunkZ)))
|
||||
{
|
||||
return ChunkZ;
|
||||
}
|
||||
}
|
||||
return MIN_int32;
|
||||
}
|
||||
|
||||
static int32 CountGapSamples(const VoxelForgeTest::FTestWorld& World,
|
||||
FIntVector Origin, int32 Step)
|
||||
{
|
||||
TSet<int32> SampleChunkZs;
|
||||
for (int32 Z = 0; Z < MesherGridSize; ++Z)
|
||||
{
|
||||
SampleChunkZs.Add(FMath::FloorToInt(
|
||||
static_cast<float>(Origin.Z + Z * Step) / CHUNK_SIZE));
|
||||
}
|
||||
int32 Count = 0;
|
||||
for (const int32 ChunkZ : SampleChunkZs)
|
||||
{
|
||||
if (World.StrateManager->IsGapChunk(FIntVector(0, 0, ChunkZ)))
|
||||
{
|
||||
++Count;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
struct FScopedConsoleIntOverride
|
||||
{
|
||||
IConsoleVariable* Variable = nullptr;
|
||||
int32 OriginalValue = 0;
|
||||
|
||||
explicit FScopedConsoleIntOverride(const TCHAR* Name, int32 Value)
|
||||
: Variable(IConsoleManager::Get().FindConsoleVariable(Name))
|
||||
{
|
||||
if (Variable != nullptr)
|
||||
{
|
||||
OriginalValue = Variable->GetInt();
|
||||
Variable->Set(Value, ECVF_SetByCode);
|
||||
}
|
||||
}
|
||||
|
||||
~FScopedConsoleIntOverride()
|
||||
{
|
||||
if (Variable != nullptr)
|
||||
{
|
||||
Variable->Set(OriginalValue, ECVF_SetByCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeDensityBlockEquivalenceTest,
|
||||
"VoxelForge.Density.OperatorBlockMatchesScalarGrid",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeDensityBlockEquivalenceTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
(void)Parameters;
|
||||
|
||||
// The production mesher initializes these once at startup. This headless test does so too,
|
||||
// then temporarily disables the fused TunnelNetwork evaluator so the real per-chunk
|
||||
// BeginDensityBlock -> BuildActiveOpList -> EvalBlock path is compared against scalar output.
|
||||
VoxelGenLOD::InitializeConsoleSwitches();
|
||||
FScopedConsoleIntOverride FusedEvaluatorOverride(TEXT("voxel.UseFusedEvaluator"), 0);
|
||||
if (FusedEvaluatorOverride.Variable == nullptr)
|
||||
{
|
||||
AddError(TEXT("voxel.UseFusedEvaluator cvar is unavailable; block route was not forced"));
|
||||
return false;
|
||||
}
|
||||
|
||||
static const int32 Seeds[] = {1337, 9176, 17015, 24854, 32693, 40532};
|
||||
static const ECaveGeneratorType Archetypes[ArchetypeCount] = {
|
||||
ECaveGeneratorType::TunnelNetwork,
|
||||
ECaveGeneratorType::FlatPlain,
|
||||
ECaveGeneratorType::CrystalChamber,
|
||||
ECaveGeneratorType::Maze,
|
||||
ECaveGeneratorType::SurfaceWorld,
|
||||
ECaveGeneratorType::VerticalShafts,
|
||||
ECaveGeneratorType::FloatingIslands,
|
||||
ECaveGeneratorType::Underwater,
|
||||
};
|
||||
|
||||
int64 GeneratorSamples = 0;
|
||||
int32 GeneratorMismatches = 0;
|
||||
FString FirstGeneratorMismatch;
|
||||
int64 DirectStackSamples = 0;
|
||||
int32 DirectStackMismatches = 0;
|
||||
FString FirstDirectStackMismatch;
|
||||
int32 DirectStackBlocks = 0;
|
||||
int32 GeneratorBlocks = 0;
|
||||
int32 ProductionArchetypeBlocks = 0;
|
||||
int32 BoundaryBlocks = 0;
|
||||
int32 NearOriginBlocks = 0;
|
||||
int32 FarOriginBlocks = 0;
|
||||
int32 GapCoverageBlocks = 0;
|
||||
int32 PassageFocusedBlocks = 0;
|
||||
int32 SpecialGapCoverageBlocks = 0;
|
||||
int32 EditedSamplesChanged = 0;
|
||||
int32 AcceptedEdits = 0;
|
||||
int32 ArchetypeLevelCounts[ArchetypeCount][6] = {};
|
||||
|
||||
for (int32 SeedIndex = 0; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
|
||||
{
|
||||
VoxelForgeTest::FTestWorld World;
|
||||
World.Build(Seeds[SeedIndex], /*gap chunks=*/2, /*operator stack=*/true);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
// The fixture uses coarse-LOD edits up to 64 voxels wide. Keep the actual diff layer in
|
||||
// the route while allowing those bounded test brushes.
|
||||
World.DiffLayer->SetBudget(/*max mods=*/0, /*max radius=*/256.0f,
|
||||
/*max accumulated volume=*/0.0f);
|
||||
|
||||
const TArray<FStrateSlot>& Slots = World.StrateManager->GetLayout();
|
||||
const TArray<FVoxelPassage>& Passages = World.StrateManager->GetPassages();
|
||||
if (Slots.Num() < ArchetypeCount || Passages.IsEmpty())
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("seed %d is missing an archetype slot or passage fixture"), Seeds[SeedIndex]));
|
||||
return false;
|
||||
}
|
||||
const int32 GapChunkZ = FindGapChunkZ(World);
|
||||
if (GapChunkZ == MIN_int32)
|
||||
{
|
||||
AddError(FString::Printf(TEXT("seed %d produced no gap chunk fixture"), Seeds[SeedIndex]));
|
||||
return false;
|
||||
}
|
||||
|
||||
FIntVector GridOrigins[GridSetCount];
|
||||
int32 GridSteps[GridSetCount] = {};
|
||||
int32 GridArchetypes[GridSetCount] = {};
|
||||
FGridComparison BaseResults[GridSetCount];
|
||||
|
||||
// One complete mesher lattice for each production archetype per seed. Rotating level by
|
||||
// seed gives every archetype all levels 0..5 over the six seeds. The sample grids touch a
|
||||
// strate edge, and cycle through origin, near-origin, and both far-field signs.
|
||||
for (int32 ArchetypeIndex = 0; ArchetypeIndex < ArchetypeCount; ++ArchetypeIndex)
|
||||
{
|
||||
const int32 Level = (SeedIndex + ArchetypeIndex) % 6;
|
||||
const int32 Step = 1 << Level;
|
||||
const FStrateSlot& Slot = Slots[ArchetypeIndex];
|
||||
const int32 TargetChunkZ = (SeedIndex & 1) ? Slot.BottomChunkZ : Slot.TopChunkZ;
|
||||
FIntVector TargetChunk;
|
||||
switch ((SeedIndex + ArchetypeIndex) % 4)
|
||||
{
|
||||
case 0: TargetChunk = FIntVector(0, 0, TargetChunkZ); break;
|
||||
case 1: TargetChunk = FIntVector(128, 96, TargetChunkZ); ++FarOriginBlocks; break;
|
||||
case 2: TargetChunk = FIntVector(8, -8, TargetChunkZ); ++NearOriginBlocks; break;
|
||||
default: TargetChunk = FIntVector(-128, -96, TargetChunkZ); ++FarOriginBlocks; break;
|
||||
}
|
||||
if ((SeedIndex + ArchetypeIndex) % 4 == 0) { ++NearOriginBlocks; }
|
||||
|
||||
const int32 GridIndex = ArchetypeIndex;
|
||||
GridOrigins[GridIndex] = MakeMesherGridOrigin(TargetChunk, Level);
|
||||
GridSteps[GridIndex] = Step;
|
||||
GridArchetypes[GridIndex] = ArchetypeIndex;
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
FVoxelOpContext Context;
|
||||
if (!BuildProductionStackForArchetype(
|
||||
World, Archetypes[ArchetypeIndex], TargetChunk, Stack, Context))
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("seed %d archetype %d production stack could not be built"),
|
||||
Seeds[SeedIndex], ArchetypeIndex));
|
||||
return false;
|
||||
}
|
||||
Context.Step = 1;
|
||||
Stack.PrepareChunk(Context);
|
||||
const FGridComparison DirectComparison = CompareStackChunkBlockWithScalar(
|
||||
Stack, GridOrigins[GridIndex], Step, TargetChunk);
|
||||
DirectStackSamples += DirectComparison.Samples;
|
||||
DirectStackMismatches += DirectComparison.Mismatches;
|
||||
++DirectStackBlocks;
|
||||
if (FirstDirectStackMismatch.IsEmpty() && DirectComparison.Mismatches > 0)
|
||||
{
|
||||
FirstDirectStackMismatch = FString::Printf(
|
||||
TEXT("seed=%d archetype=%d level=%d chunk=%s %s"),
|
||||
Seeds[SeedIndex], ArchetypeIndex, Level, *TargetChunk.ToString(),
|
||||
*DirectComparison.FirstMismatch);
|
||||
}
|
||||
|
||||
++ArchetypeLevelCounts[ArchetypeIndex][Level];
|
||||
++ProductionArchetypeBlocks;
|
||||
++BoundaryBlocks;
|
||||
|
||||
if (!World.StrateManager->UsesOperatorStackForChunk(TargetChunk)
|
||||
|| World.StrateManager->GetGeneratorTypeForChunk(TargetChunk)
|
||||
!= Archetypes[ArchetypeIndex])
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("seed %d archetype %d target chunk did not route to its production operator stack"),
|
||||
Seeds[SeedIndex], ArchetypeIndex));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Passage-mouth lattices centered in a real inter-strate gap exercise the routed
|
||||
// TunnelNetwork/Underwater block implementation through boundaries and bedrock holes.
|
||||
for (int32 PassageGrid = 0; PassageGrid < PassageGridCount; ++PassageGrid)
|
||||
{
|
||||
const int32 GridIndex = ArchetypeCount + PassageGrid;
|
||||
const int32 ArchetypeIndex = PassageGrid == 0 ? 0 : 7;
|
||||
const int32 Level = (SeedIndex + (PassageGrid == 0 ? 0 : 7)) % 6;
|
||||
const int32 Step = 1 << Level;
|
||||
const FVoxelPassage& Passage = Passages[(SeedIndex + PassageGrid) % Passages.Num()];
|
||||
const FVector Focus = PassageGrid == 0 ? Passage.UpperPoint : Passage.LowerPoint;
|
||||
const FIntVector TargetChunk(
|
||||
FMath::FloorToInt(Focus.X / CHUNK_SIZE),
|
||||
FMath::FloorToInt(Focus.Y / CHUNK_SIZE),
|
||||
GapChunkZ);
|
||||
GridOrigins[GridIndex] = MakeMesherGridOrigin(TargetChunk, Level);
|
||||
GridSteps[GridIndex] = Step;
|
||||
GridArchetypes[GridIndex] = ArchetypeIndex;
|
||||
++PassageFocusedBlocks;
|
||||
if (CountGapSamples(World, GridOrigins[GridIndex], Step) > 0)
|
||||
{
|
||||
++GapCoverageBlocks;
|
||||
++SpecialGapCoverageBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
// No-edit grids. In the two canonical network archetypes this enters the actual worker
|
||||
// block session, which partitions each mesher grid by resolved chunk before running the
|
||||
// active operator list. The six legacy density routes still run their production scalar
|
||||
// evaluator, as they do outside the block-safe network branch.
|
||||
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
|
||||
{
|
||||
BaseResults[GridIndex] = CompareGeneratorBlockWithScalar(
|
||||
*World.Generator.Get(), GridOrigins[GridIndex], GridSteps[GridIndex]);
|
||||
GeneratorSamples += BaseResults[GridIndex].Samples;
|
||||
GeneratorMismatches += BaseResults[GridIndex].Mismatches;
|
||||
++GeneratorBlocks;
|
||||
if (FirstGeneratorMismatch.IsEmpty() && BaseResults[GridIndex].Mismatches > 0)
|
||||
{
|
||||
FirstGeneratorMismatch = FString::Printf(
|
||||
TEXT("seed=%d archetype=%d level=%d edited=0 origin=%s %s"),
|
||||
Seeds[SeedIndex], GridArchetypes[GridIndex],
|
||||
FMath::FloorLog2(GridSteps[GridIndex]), *GridOrigins[GridIndex].ToString(),
|
||||
*BaseResults[GridIndex].FirstMismatch);
|
||||
}
|
||||
}
|
||||
|
||||
// Put one real diff-layer box edit in every mesher grid, then compare the same exact
|
||||
// density lattices again. The scalar pass below runs with these player edits present.
|
||||
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
|
||||
{
|
||||
const int32 Step = GridSteps[GridIndex];
|
||||
FVoxelModification Edit;
|
||||
Edit.Shape = EVoxelBrushShape::Box;
|
||||
Edit.Center = FVector(GridOrigins[GridIndex]) + FVector(17 * Step);
|
||||
Edit.BoxExtent = FVector(2.0f * Step);
|
||||
Edit.Falloff = 1.0f;
|
||||
Edit.Strength = -13.0f;
|
||||
const TArray<FIntVector> ChangedChunks = World.DiffLayer->ApplyModification(Edit);
|
||||
if (ChangedChunks.IsEmpty())
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("seed %d edit for grid %d archetype %d was rejected"),
|
||||
Seeds[SeedIndex], GridIndex, GridArchetypes[GridIndex]));
|
||||
return false;
|
||||
}
|
||||
++AcceptedEdits;
|
||||
}
|
||||
|
||||
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
|
||||
{
|
||||
const FGridComparison Edited = CompareGeneratorBlockWithScalar(
|
||||
*World.Generator.Get(), GridOrigins[GridIndex], GridSteps[GridIndex]);
|
||||
GeneratorSamples += Edited.Samples;
|
||||
GeneratorMismatches += Edited.Mismatches;
|
||||
++GeneratorBlocks;
|
||||
if (FirstGeneratorMismatch.IsEmpty() && Edited.Mismatches > 0)
|
||||
{
|
||||
FirstGeneratorMismatch = FString::Printf(
|
||||
TEXT("seed=%d archetype=%d level=%d edited=1 origin=%s %s"),
|
||||
Seeds[SeedIndex], GridArchetypes[GridIndex],
|
||||
FMath::FloorLog2(GridSteps[GridIndex]), *GridOrigins[GridIndex].ToString(),
|
||||
*Edited.FirstMismatch);
|
||||
}
|
||||
|
||||
const int32 SharedCount = FMath::Min(
|
||||
Edited.BlockDensities.Num(), BaseResults[GridIndex].BlockDensities.Num());
|
||||
for (int32 SampleIndex = 0; SampleIndex < SharedCount; ++SampleIndex)
|
||||
{
|
||||
if (!SameFloatBits(Edited.BlockDensities[SampleIndex],
|
||||
BaseResults[GridIndex].BlockDensities[SampleIndex]))
|
||||
{
|
||||
++EditedSamplesChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool bEveryArchetypeCoveredEveryLevel = true;
|
||||
for (int32 ArchetypeIndex = 0; ArchetypeIndex < ArchetypeCount; ++ArchetypeIndex)
|
||||
{
|
||||
for (int32 Level = 0; Level < 6; ++Level)
|
||||
{
|
||||
bEveryArchetypeCoveredEveryLevel &= ArchetypeLevelCounts[ArchetypeIndex][Level] > 0;
|
||||
}
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("BeginDensityBlock grid vs scalar GetDensityAt: seeds=%d archetype_grids=%d all_archetype_levels=%d boundary_grids=%d near_origin=%d far_origin=%d blocks=%d samples=%lld mismatches=%d gap_grids=%d passage_focused=%d accepted_edits=%d edited_samples_changed=%d first=%s"),
|
||||
UE_ARRAY_COUNT(Seeds), ProductionArchetypeBlocks,
|
||||
bEveryArchetypeCoveredEveryLevel ? 1 : 0, BoundaryBlocks, NearOriginBlocks,
|
||||
FarOriginBlocks, GeneratorBlocks, static_cast<long long>(GeneratorSamples),
|
||||
GeneratorMismatches, GapCoverageBlocks, PassageFocusedBlocks, AcceptedEdits,
|
||||
EditedSamplesChanged,
|
||||
FirstGeneratorMismatch.IsEmpty() ? TEXT("none") : *FirstGeneratorMismatch));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("chunk-local direct EvalBlock vs EvalInternal: blocks=%d mesher_samples=%lld mismatches=%d first=%s"),
|
||||
DirectStackBlocks, static_cast<long long>(DirectStackSamples),
|
||||
DirectStackMismatches,
|
||||
FirstDirectStackMismatch.IsEmpty() ? TEXT("none") : *FirstDirectStackMismatch));
|
||||
|
||||
TestEqual(TEXT("all eight production operator archetypes get six complete mesher grids"),
|
||||
ProductionArchetypeBlocks,
|
||||
static_cast<int32>(UE_ARRAY_COUNT(Seeds)) * ArchetypeCount);
|
||||
TestTrue(TEXT("every production operator archetype covers every meshing level 0..5"),
|
||||
bEveryArchetypeCoveredEveryLevel);
|
||||
TestEqual(TEXT("all production archetype grids touch a strate boundary"),
|
||||
BoundaryBlocks, ProductionArchetypeBlocks);
|
||||
TestTrue(TEXT("test includes near-origin and both-sign far-field grids"),
|
||||
NearOriginBlocks > 0 && FarOriginBlocks > 0);
|
||||
TestEqual(TEXT("each passage-focused grid includes a real gap sample"),
|
||||
SpecialGapCoverageBlocks,
|
||||
static_cast<int32>(UE_ARRAY_COUNT(Seeds)) * PassageGridCount);
|
||||
TestEqual(TEXT("passage-focused grids cover both network archetypes per seed"),
|
||||
PassageFocusedBlocks,
|
||||
static_cast<int32>(UE_ARRAY_COUNT(Seeds)) * PassageGridCount);
|
||||
TestEqual(TEXT("all block-session density grids match scalar output bit-for-bit"),
|
||||
GeneratorMismatches, 0);
|
||||
TestEqual(TEXT("all production stack chunk blocks match scalar bit-for-bit"),
|
||||
DirectStackMismatches, 0);
|
||||
TestEqual(TEXT("one valid player edit was applied to every tested grid"),
|
||||
AcceptedEdits, static_cast<int32>(UE_ARRAY_COUNT(Seeds)) * GridSetCount);
|
||||
TestTrue(TEXT("player edits alter samples consumed by the mesher"),
|
||||
EditedSamplesChanged > 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
File diff suppressed because it is too large.
Load diff
@@ -24,6 +24,27 @@ namespace VoxelClipmapDesiredTiles
|
||||
FloorDiv(Value.Z, Divisor));
|
||||
}
|
||||
|
||||
FORCEINLINE int32 CeilDiv(int32 Value, int32 Divisor)
|
||||
{
|
||||
return -FloorDiv(-Value, Divisor);
|
||||
}
|
||||
|
||||
FORCEINLINE void AlignedRingBounds(const FIntVector& Center, int32 Radius, int32 Alignment,
|
||||
FIntVector& OutMin, FIntVector& OutMax)
|
||||
{
|
||||
const auto AlignAxis = [Radius, Alignment](int32 CenterAxis,
|
||||
int32& OutAxisMin, int32& OutAxisMax)
|
||||
{
|
||||
const int32 BaseMin = CenterAxis - Radius;
|
||||
const int32 BaseEnd = CenterAxis + Radius + 1;
|
||||
OutAxisMin = FloorDiv(BaseMin, Alignment) * Alignment;
|
||||
OutAxisMax = CeilDiv(BaseEnd, Alignment) * Alignment - 1;
|
||||
};
|
||||
AlignAxis(Center.X, OutMin.X, OutMax.X);
|
||||
AlignAxis(Center.Y, OutMin.Y, OutMax.Y);
|
||||
AlignAxis(Center.Z, OutMin.Z, OutMax.Z);
|
||||
}
|
||||
|
||||
FORCEINLINE int32 OuterShellRadius(const FParameters& Parameters, int32 Radius, int32 MaxLevel)
|
||||
{
|
||||
if (Parameters.RenderDistanceChunks <= 0)
|
||||
@@ -81,18 +102,35 @@ namespace VoxelClipmapDesiredTiles
|
||||
OuterShell(Parameters, Radius, MaxLevel, SheetLevel, SheetRadius);
|
||||
const bool bSheetRing = SheetLevel > MaxLevel;
|
||||
const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel;
|
||||
FIntVector PreviousRingMin = FIntVector::ZeroValue;
|
||||
FIntVector PreviousRingMax = FIntVector::ZeroValue;
|
||||
FIntVector MCRingMin = FIntVector::ZeroValue;
|
||||
FIntVector MCRingMax = FIntVector::ZeroValue;
|
||||
|
||||
for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level)
|
||||
{
|
||||
const int32 Pow = 1 << Level;
|
||||
const FIntVector CenterLevel = FloorDiv(Center, Pow);
|
||||
const FIntVector CenterFiner = Level > 0
|
||||
? FloorDiv(Center, Pow >> 1) : FIntVector::ZeroValue;
|
||||
const int32 LevelRadius = (Level == MaxLevel && !bSheetRing)
|
||||
? OuterRadius : Radius;
|
||||
|
||||
int32 DeltaZMin = -LevelRadius;
|
||||
int32 DeltaZMax = LevelRadius;
|
||||
// Every finer shell must end on a complete child-pair boundary so a coarser tile is
|
||||
// either wholly inside it or wholly outside it. The final MC ring uses the sheet's
|
||||
// grouping width when a sheet ring follows it; otherwise the ordinary LOD boundary
|
||||
// only needs pair alignment.
|
||||
const int32 RingAlignment = Level < MaxMCRingLevel
|
||||
? 2
|
||||
: (bSheetRing ? (1 << (SheetLevel - MaxMCRingLevel)) : 1);
|
||||
FIntVector RingMin, RingMax;
|
||||
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
|
||||
if (Level == MaxMCRingLevel)
|
||||
{
|
||||
MCRingMin = RingMin;
|
||||
MCRingMax = RingMax;
|
||||
}
|
||||
|
||||
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
|
||||
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
|
||||
if (LevelRadius > Radius && MinChunkZ != MIN_int32)
|
||||
{
|
||||
DeltaZMin = FMath::Max(DeltaZMin, FloorDiv(MinChunkZ, Pow) - CenterLevel.Z);
|
||||
@@ -100,8 +138,10 @@ namespace VoxelClipmapDesiredTiles
|
||||
}
|
||||
|
||||
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
|
||||
for (int32 DeltaY = -LevelRadius; DeltaY <= LevelRadius; ++DeltaY)
|
||||
for (int32 DeltaX = -LevelRadius; DeltaX <= LevelRadius; ++DeltaX)
|
||||
for (int32 DeltaY = RingMin.Y - CenterLevel.Y;
|
||||
DeltaY <= RingMax.Y - CenterLevel.Y; ++DeltaY)
|
||||
for (int32 DeltaX = RingMin.X - CenterLevel.X;
|
||||
DeltaX <= RingMax.X - CenterLevel.X; ++DeltaX)
|
||||
{
|
||||
const FIntVector Tile = CenterLevel + FIntVector(DeltaX, DeltaY, DeltaZ);
|
||||
const int32 TileMinZ = Tile.Z << Level;
|
||||
@@ -114,12 +154,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
if (Level > 0)
|
||||
{
|
||||
const bool bCovered =
|
||||
(2 * Tile.X >= CenterFiner.X - Radius)
|
||||
&& (2 * Tile.X + 1 <= CenterFiner.X + Radius)
|
||||
&& (2 * Tile.Y >= CenterFiner.Y - Radius)
|
||||
&& (2 * Tile.Y + 1 <= CenterFiner.Y + Radius)
|
||||
&& (2 * Tile.Z >= CenterFiner.Z - Radius)
|
||||
&& (2 * Tile.Z + 1 <= CenterFiner.Z + Radius);
|
||||
(2 * Tile.X >= PreviousRingMin.X)
|
||||
&& (2 * Tile.X + 1 <= PreviousRingMax.X)
|
||||
&& (2 * Tile.Y >= PreviousRingMin.Y)
|
||||
&& (2 * Tile.Y + 1 <= PreviousRingMax.Y)
|
||||
&& (2 * Tile.Z >= PreviousRingMin.Z)
|
||||
&& (2 * Tile.Z + 1 <= PreviousRingMax.Z);
|
||||
if (bCovered)
|
||||
{
|
||||
continue;
|
||||
@@ -128,14 +168,17 @@ namespace VoxelClipmapDesiredTiles
|
||||
|
||||
OutTiles.Emplace(Tile, Level);
|
||||
}
|
||||
|
||||
PreviousRingMin = RingMin;
|
||||
PreviousRingMax = RingMax;
|
||||
}
|
||||
|
||||
if (bSheetRing)
|
||||
{
|
||||
const int32 SheetPow = 1 << SheetLevel;
|
||||
const FIntVector CenterSheet = FloorDiv(Center, SheetPow);
|
||||
const FIntVector CenterMC = FloorDiv(Center, 1 << MaxMCRingLevel);
|
||||
const int32 LevelDelta = SheetLevel - MaxMCRingLevel;
|
||||
const int32 MCPerSheet = 1 << LevelDelta;
|
||||
|
||||
int32 DeltaZMin = -SheetRadius;
|
||||
int32 DeltaZMax = SheetRadius;
|
||||
@@ -160,12 +203,12 @@ namespace VoxelClipmapDesiredTiles
|
||||
}
|
||||
|
||||
const bool bCovered =
|
||||
((Tile.X << LevelDelta) >= CenterMC.X - Radius)
|
||||
&& ((((Tile.X + 1) << LevelDelta) - 1) <= CenterMC.X + Radius)
|
||||
&& ((Tile.Y << LevelDelta) >= CenterMC.Y - Radius)
|
||||
&& ((((Tile.Y + 1) << LevelDelta) - 1) <= CenterMC.Y + Radius)
|
||||
&& ((Tile.Z << LevelDelta) >= CenterMC.Z - Radius)
|
||||
&& ((((Tile.Z + 1) << LevelDelta) - 1) <= CenterMC.Z + Radius);
|
||||
(Tile.X * MCPerSheet >= MCRingMin.X)
|
||||
&& ((Tile.X + 1) * MCPerSheet - 1 <= MCRingMax.X)
|
||||
&& (Tile.Y * MCPerSheet >= MCRingMin.Y)
|
||||
&& ((Tile.Y + 1) * MCPerSheet - 1 <= MCRingMax.Y)
|
||||
&& (Tile.Z * MCPerSheet >= MCRingMin.Z)
|
||||
&& ((Tile.Z + 1) * MCPerSheet - 1 <= MCRingMax.Z);
|
||||
if (!bCovered)
|
||||
{
|
||||
OutTiles.Emplace(Tile, SheetLevel);
|
||||
|
||||
@@ -3407,6 +3407,16 @@ namespace
|
||||
// unknown/evicted proof data therefore falls back to sampling and cannot change the mesh.
|
||||
constexpr uint64 SharedRoomGraphCacheBudgetBytes = 256ull * 1024ull * 1024ull;
|
||||
|
||||
static TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe>
|
||||
SnapshotSharedRoomGraphCache(
|
||||
const TSharedPtr<FSharedRoomGraphCacheEntry, ESPMode::ThreadSafe>& Entry)
|
||||
{
|
||||
FScopeLock Lock(&GSharedRoomGraphCacheMutex);
|
||||
return Entry.IsValid()
|
||||
? Entry->Cache
|
||||
: TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe>();
|
||||
}
|
||||
|
||||
int32 SharedRoomGraphCacheBuildBank(int32 RegionSize)
|
||||
{
|
||||
return RegionSize <= 2 * CHUNK_SIZE ? 0 : 1;
|
||||
@@ -3593,8 +3603,11 @@ namespace
|
||||
Entry->RegionSize = RegionSize;
|
||||
Entry->LastUse = 0;
|
||||
Entry->AllocatedBytes = AllocatedBytes;
|
||||
{
|
||||
FScopeLock Lock(&GSharedRoomGraphCacheMutex);
|
||||
Entry->Cache = BuiltCache;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FScopeLock Lock(&GSharedRoomGraphCacheMutex);
|
||||
@@ -3647,7 +3660,7 @@ namespace
|
||||
return Entry;
|
||||
}
|
||||
|
||||
if (Entry->Cache.IsValid())
|
||||
if (SnapshotSharedRoomGraphCache(Entry).IsValid())
|
||||
{
|
||||
return Entry;
|
||||
}
|
||||
@@ -4119,9 +4132,13 @@ namespace
|
||||
|
||||
if (SampleStep > 1)
|
||||
{
|
||||
TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe> SharedRoomGraphCache =
|
||||
S.SharedCacheEntry.IsValid()
|
||||
? SnapshotSharedRoomGraphCache(S.SharedCacheEntry)
|
||||
: TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe>();
|
||||
const bool bSharedCacheMatches =
|
||||
S.SharedCacheEntry.IsValid()
|
||||
&& S.SharedCacheEntry->Cache.IsValid()
|
||||
&& SharedRoomGraphCache.IsValid()
|
||||
&& S.SharedCacheEntry->Seed == SeedU
|
||||
&& S.SharedCacheEntry->StrateIndex == StrateIdx
|
||||
&& S.SharedCacheEntry->ParamsFingerprint == ParamsFingerprint
|
||||
@@ -4143,8 +4160,9 @@ namespace
|
||||
(float)(RegionMinY + RegionSize) + CacheExpansion,
|
||||
P,
|
||||
ERoomGraphBuildSite::OpShared);
|
||||
SharedRoomGraphCache = SnapshotSharedRoomGraphCache(S.SharedCacheEntry);
|
||||
}
|
||||
S.ActiveCache = S.SharedCacheEntry->Cache.Get();
|
||||
S.ActiveCache = SharedRoomGraphCache.Get();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -5109,7 +5127,9 @@ namespace
|
||||
* VOXEL_NOISE_SCALE * VF_PerlinAbsBound + 2.0f),
|
||||
P,
|
||||
ERoomGraphBuildSite::ClassifierShared);
|
||||
B.ActiveCache = SharedCache->Cache.Get();
|
||||
const TSharedPtr<const FChunkSDFCache, ESPMode::ThreadSafe>
|
||||
SharedRoomGraphCache = SnapshotSharedRoomGraphCache(SharedCache);
|
||||
B.ActiveCache = SharedRoomGraphCache.Get();
|
||||
B.CacheWindowSharedEntry = SharedCache;
|
||||
B.CacheWindowSharedRegionMinX = SharedRegionMinX;
|
||||
B.CacheWindowSharedRegionMinY = SharedRegionMinY;
|
||||
@@ -5165,7 +5185,8 @@ namespace
|
||||
}
|
||||
else if (B.bCacheWindowUsesShared)
|
||||
{
|
||||
B.ActiveCache = B.CacheWindowSharedEntry->Cache.Get();
|
||||
B.ActiveCache = SnapshotSharedRoomGraphCache(
|
||||
B.CacheWindowSharedEntry).Get();
|
||||
}
|
||||
// Large root boxes are only a cache warm-up. Their interval is intentionally still
|
||||
// unknown; proof resumes at bounded descendants where all bounds below are finite.
|
||||
@@ -6753,7 +6774,12 @@ namespace
|
||||
FVoxelBoxSdfInterval Own;
|
||||
if (bUseExactLatticeWarp)
|
||||
{
|
||||
Own.Set(ExactSdfLower, FLT_MAX);
|
||||
// Exact per-primitive scans are a tightening only. Their candidate-domain
|
||||
// filters can miss a warped lattice query near a primitive, so retain the
|
||||
// broad lower bound over the AABB that contains every warped sample. The
|
||||
// minimum stays useful when both agree and remains safe when an exact scan is
|
||||
// incomplete.
|
||||
Own.Set(FMath::Min(ExactSdfLower, CheapSdfLower), FLT_MAX);
|
||||
}
|
||||
else if (!bAnyPrimitive)
|
||||
{
|
||||
|
||||
@@ -6328,6 +6328,34 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
static_cast<float>(Box.Max.Z - Box.Min.Z));
|
||||
const bool bSmallFinalFieldBox = bNeedsFinalField
|
||||
&& FinalFieldBoxExtent <= 8.0f;
|
||||
|
||||
// A room-graph SDF interval can be conservative at the parent while a fine child still
|
||||
// resolves uniformly through a state-aware SDF writer (for example the worm mask). Before
|
||||
// using that fine interval to skip geometry, certify the exact MC lattice in the child.
|
||||
// This runs only after a mixed parent has been refined to a small room-graph child; clear
|
||||
// uniform roots and all non-room stacks keep the interval-only fast path.
|
||||
const bool bNeedsExactRoomIntervalCheck = bHasRoomGeometry
|
||||
&& Context.bUseLatticeProof && Context.bTightenWarpProof
|
||||
&& !bNeedsFinalField && Whole != EVoxelTileClass::Mixed
|
||||
&& Depth >= 3 && FinalFieldBoxExtent <= 8.0f;
|
||||
if (bNeedsExactRoomIntervalCheck)
|
||||
{
|
||||
int32 ExactIX0 = 0, ExactIY0 = 0, ExactIZ0 = 0;
|
||||
int32 ExactIX1 = 0, ExactIY1 = 0, ExactIZ1 = 0;
|
||||
int64 ExactCount = 0;
|
||||
if (GetLatticeBounds(Box, ExactIX0, ExactIY0, ExactIZ0,
|
||||
ExactIX1, ExactIY1, ExactIZ1, ExactCount)
|
||||
&& ExactCount <= 8192)
|
||||
{
|
||||
if (Stats) { ++Stats->ExactCoreIntervalChecks; }
|
||||
const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, false);
|
||||
if (Exact != Whole && Stats)
|
||||
{
|
||||
++Stats->ExactCoreIntervalDisagreements;
|
||||
}
|
||||
return Exact;
|
||||
}
|
||||
}
|
||||
if (bSmallFinalFieldBox)
|
||||
{
|
||||
int32 ExactIX0 = 0, ExactIY0 = 0, ExactIZ0 = 0;
|
||||
@@ -7079,8 +7107,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
|
||||
|
||||
// ── Structural passage/origin guards for non-open-air categories. ──
|
||||
// Carvers kill AllSolid but never add rock; landing and origin support floors kill AllAir.
|
||||
if (StrateManager->AnyPassageNearLattice(
|
||||
TileVoxelBox, OriginVoxels, Step))
|
||||
const bool bHasPassageCarvingCandidate =
|
||||
StrateManager->AnyPassageNearLattice(
|
||||
TileVoxelBox, OriginVoxels, Step);
|
||||
if (bHasPassageCarvingCandidate)
|
||||
{
|
||||
bCanSolid = false;
|
||||
}
|
||||
@@ -7327,10 +7357,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
|
||||
&& CaveMinZ == MinZ && CaveMaxZ == MaxZ;
|
||||
if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllSolid)
|
||||
{
|
||||
// The stack verdict covers the complete cave-only MC lattice. Its exact structural
|
||||
// post path was supplied above, so a broad conservative passage guard must not force
|
||||
// this already-certified solid tile through GenerateMesh.
|
||||
bCanSolid = true;
|
||||
// The operator stack does not own the final passage carve. Keep
|
||||
// its AllSolid shortcut only when the exact-lattice passage guard
|
||||
// found no modifier or body-envelope write in this tile.
|
||||
bCanSolid = !bHasPassageCarvingCandidate;
|
||||
bCanAir = false;
|
||||
}
|
||||
else if (bCaveOnlyTile && StackVerdict == EVoxelTileClass::AllAir)
|
||||
|
||||
@@ -98,7 +98,9 @@ namespace
|
||||
int32 AllAir = 0;
|
||||
int32 AllSolid = 0;
|
||||
int32 Mixed = 0;
|
||||
int64 Vertices = 0;
|
||||
int64 Triangles = 0;
|
||||
uint64 GeometryBytes = 0;
|
||||
|
||||
for (const FTileSample& Sample : Samples)
|
||||
{
|
||||
@@ -108,7 +110,9 @@ namespace
|
||||
if (Sample.Verdict == 0) ++Mixed;
|
||||
else if (Sample.Verdict == 1) ++AllSolid;
|
||||
else if (Sample.Verdict == 2) ++AllAir;
|
||||
Vertices += Sample.Vertices;
|
||||
Triangles += Sample.Triangles;
|
||||
GeometryBytes += Sample.GeometryBytes;
|
||||
Request.Add(Sample.RequestToApplySeconds);
|
||||
Queue.Add(Sample.QueueWaitSeconds);
|
||||
Generation.Add(Sample.GenerationSeconds);
|
||||
@@ -118,14 +122,16 @@ namespace
|
||||
}
|
||||
|
||||
return FString::Printf(
|
||||
TEXT("{\"count\":%d,\"empty\":%d,\"all_air\":%d,\"all_solid\":%d,\"mixed\":%d,\"triangles\":%lld,"
|
||||
TEXT("{\"count\":%d,\"empty\":%d,\"all_air\":%d,\"all_solid\":%d,\"mixed\":%d,\"vertices\":%lld,\"triangles\":%lld,\"geometry_bytes\":%llu,"
|
||||
"\"request_apply_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"queue_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"generation_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"classify_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"mesh_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"apply_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s}}"),
|
||||
Count, Empty, AllAir, AllSolid, Mixed, static_cast<long long>(Triangles),
|
||||
Count, Empty, AllAir, AllSolid, Mixed,
|
||||
static_cast<long long>(Vertices), static_cast<long long>(Triangles),
|
||||
static_cast<unsigned long long>(GeometryBytes),
|
||||
*Number(Percentile(Request, 0.50)), *Number(Percentile(Request, 0.95)), *Number(Maximum(Request)),
|
||||
*Number(Percentile(Queue, 0.50)), *Number(Percentile(Queue, 0.95)), *Number(Maximum(Queue)),
|
||||
*Number(Percentile(Generation, 0.50)), *Number(Percentile(Generation, 0.95)), *Number(Maximum(Generation)),
|
||||
@@ -260,12 +266,13 @@ void RecordTile(const FTileSample& Sample)
|
||||
GState->Samples.Add(Sample);
|
||||
++GState->TileRecordsWritten;
|
||||
WriteEventLocked(*GState, TEXT("tile"), FString::Printf(
|
||||
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"triangles\":%d,"
|
||||
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
|
||||
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,"
|
||||
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
|
||||
Sample.TileX, Sample.TileY, Sample.TileZ,
|
||||
Sample.Level, Sample.Verdict, Sample.bSealedSolidProof ? 1 : 0,
|
||||
Sample.bEmpty ? 1 : 0, Sample.Triangles,
|
||||
Sample.bEmpty ? 1 : 0, Sample.Vertices, Sample.Triangles,
|
||||
static_cast<unsigned long long>(Sample.GeometryBytes),
|
||||
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
||||
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
||||
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -504,6 +504,8 @@ struct FVoxelTileClassificationStats
|
||||
uint32 MaxRefinementDepth = 0;
|
||||
uint32 ExactCoreSamples = 0;
|
||||
uint32 ExactFinalSamples = 0;
|
||||
uint32 ExactCoreIntervalChecks = 0;
|
||||
uint32 ExactCoreIntervalDisagreements = 0;
|
||||
uint32 ExactCoreCacheHits = 0;
|
||||
uint32 ExactFinalCacheHits = 0;
|
||||
uint32 ExactCoreLeaves = 0;
|
||||
|
||||
@@ -23,6 +23,11 @@ namespace VoxelPassageGeometry
|
||||
constexpr float MinimumRoomHeightVoxels = 12.0f; // 3 m
|
||||
constexpr float LandingFloorThicknessVoxels = 3.0f;
|
||||
constexpr float MaxStepHeightVoxels = 1.8f; // 45 cm at 25 cm/voxel
|
||||
// The A-to-B walk corridor is budgeted as a full player-width rectangle. Its vertical
|
||||
// envelope starts above the legal step and extends one player height above that raised floor.
|
||||
constexpr float WalkCorridorPlayerWidthVoxels = 1.36f;
|
||||
constexpr float WalkCorridorPlayerHeightVoxels = 7.0f;
|
||||
constexpr float WalkCorridorLatticeMarginVoxels = 1.0f;
|
||||
constexpr float SealSafetyMarginVoxels = 4.0f;
|
||||
constexpr float RoomRoundingVoxels = 1.5f;
|
||||
constexpr float LandingCarveBlendVoxels = 4.0f;
|
||||
@@ -103,6 +108,25 @@ namespace VoxelPassageGeometry
|
||||
return Centreline.Z - FMath::Abs(Radius);
|
||||
}
|
||||
|
||||
FORCEINLINE float NativePassageFloorSupportRadius(float StartRadius, float EndRadius)
|
||||
{
|
||||
return FMath::Max(
|
||||
FMath::Min(FMath::Abs(StartRadius), FMath::Abs(EndRadius)) - 0.5f,
|
||||
PlayerRadiusVoxels);
|
||||
}
|
||||
|
||||
// Keep two switchback floor branches apart far enough that the upper branch's final floor
|
||||
// post cannot enter the lower player's full-width walk corridor. The one-voxel margin covers
|
||||
// the scalar lattice/interpolation boundary; Eval displacement is already followed by the
|
||||
// MC-facing tunnel and native-floor stages before collision density is returned.
|
||||
FORCEINLINE float WalkableTunnelTurnLegSeparation(
|
||||
float StartRadius, float EndRadius)
|
||||
{
|
||||
return NativePassageFloorSupportRadius(StartRadius, EndRadius)
|
||||
+ 0.5f * WalkCorridorPlayerWidthVoxels
|
||||
+ WalkCorridorLatticeMarginVoxels;
|
||||
}
|
||||
|
||||
// Vertical-shaft tree links are round air capsules, but the player-fit graph needs a
|
||||
// deterministic support plane to walk between shafts. Keep that plane on the first
|
||||
// seal-safe ledge shared by the whole strate, so every tree edge has the same walk level.
|
||||
@@ -350,6 +374,7 @@ namespace VoxelPassageGeometry
|
||||
const FVector* ControlPointData = ControlPoints.GetData();
|
||||
const float* ControlRadiusData = ControlRadii.GetData();
|
||||
float BestDistanceSquared = FLT_MAX;
|
||||
float BestHorizontalDistanceSquared = FLT_MAX;
|
||||
bool bFoundSegment = false;
|
||||
for (int32 SegmentIndex = 0;
|
||||
SegmentIndex + 1 < ControlPointCount;
|
||||
@@ -368,28 +393,58 @@ namespace VoxelPassageGeometry
|
||||
continue;
|
||||
}
|
||||
|
||||
const double Dot = (QueryX - AXY_X) * DeltaX
|
||||
const double HorizontalDot = (QueryX - AXY_X) * DeltaX
|
||||
+ (QueryY - AXY_Y) * DeltaY;
|
||||
const float T = FMath::Clamp(
|
||||
Dot / LengthSquared,
|
||||
const float HorizontalT = FMath::Clamp(
|
||||
HorizontalDot / LengthSquared,
|
||||
0.0f, 1.0f);
|
||||
const double ClosestX = AXY_X + DeltaX * T;
|
||||
const double ClosestY = AXY_Y + DeltaY * T;
|
||||
const double ClosestX = AXY_X + DeltaX * HorizontalT;
|
||||
const double ClosestY = AXY_Y + DeltaY * HorizontalT;
|
||||
const double DistanceX = QueryX - ClosestX;
|
||||
const double DistanceY = QueryY - ClosestY;
|
||||
const float DistanceSquared = static_cast<float>(
|
||||
const float HorizontalDistanceSquared = static_cast<float>(
|
||||
DistanceX * DistanceX + DistanceY * DistanceY);
|
||||
|
||||
const float StartRadius = FMath::Abs(ControlRadiusData[SegmentIndex]);
|
||||
const float EndRadius = FMath::Abs(ControlRadiusData[SegmentIndex + 1]);
|
||||
const float StartFloorZ = TunnelFloorZ(A, StartRadius);
|
||||
const float EndFloorZ = TunnelFloorZ(B, EndRadius);
|
||||
const double FloorDeltaZ = static_cast<double>(EndFloorZ - StartFloorZ);
|
||||
const double FullLengthSquared =
|
||||
LengthSquared + FloorDeltaZ * FloorDeltaZ;
|
||||
if (!(FullLengthSquared > KINDA_SMALL_NUMBER))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// A switchback may place two different floors at the same XY. Select the segment
|
||||
// nearest to the complete query point on the floor curve, so a lower passage leg
|
||||
// cannot inherit the floor height of a vertically separated upper leg. Keep floor
|
||||
// interpolation horizontal after choosing the branch: FloorZ remains a function of XY.
|
||||
const double FullDot = HorizontalDot
|
||||
+ (static_cast<double>(Position.Z) - StartFloorZ) * FloorDeltaZ;
|
||||
const float SpatialT = FMath::Clamp(
|
||||
FullDot / FullLengthSquared,
|
||||
0.0f, 1.0f);
|
||||
const double SpatialX = AXY_X + DeltaX * SpatialT;
|
||||
const double SpatialY = AXY_Y + DeltaY * SpatialT;
|
||||
const double SpatialZ = StartFloorZ + FloorDeltaZ * SpatialT;
|
||||
const double SpatialDistanceX = QueryX - SpatialX;
|
||||
const double SpatialDistanceY = QueryY - SpatialY;
|
||||
const double SpatialDistanceZ = Position.Z - SpatialZ;
|
||||
const float DistanceSquared = static_cast<float>(
|
||||
SpatialDistanceX * SpatialDistanceX
|
||||
+ SpatialDistanceY * SpatialDistanceY
|
||||
+ SpatialDistanceZ * SpatialDistanceZ);
|
||||
if (DistanceSquared >= BestDistanceSquared)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float StartRadius = FMath::Abs(ControlRadiusData[SegmentIndex]);
|
||||
const float EndRadius = FMath::Abs(ControlRadiusData[SegmentIndex + 1]);
|
||||
OutFloorZ = FMath::Lerp(
|
||||
TunnelFloorZ(A, StartRadius),
|
||||
TunnelFloorZ(B, EndRadius),
|
||||
T);
|
||||
StartFloorZ,
|
||||
EndFloorZ,
|
||||
HorizontalT);
|
||||
OutSupportRadius = FMath::Max(
|
||||
FMath::Min(StartRadius, EndRadius) - 0.5f,
|
||||
PlayerRadiusVoxels);
|
||||
@@ -398,6 +453,7 @@ namespace VoxelPassageGeometry
|
||||
*OutSegmentIndex = SegmentIndex;
|
||||
}
|
||||
BestDistanceSquared = DistanceSquared;
|
||||
BestHorizontalDistanceSquared = HorizontalDistanceSquared;
|
||||
bFoundSegment = true;
|
||||
}
|
||||
|
||||
@@ -405,7 +461,7 @@ namespace VoxelPassageGeometry
|
||||
&& VoxelMath::IsFinite(OutFloorZ)
|
||||
&& VoxelMath::IsFinite(OutSupportRadius)
|
||||
&& OutSupportRadius > 0.0f
|
||||
&& BestDistanceSquared <= FMath::Square(OutSupportRadius);
|
||||
&& BestHorizontalDistanceSquared <= FMath::Square(OutSupportRadius);
|
||||
}
|
||||
|
||||
struct FOriginLandingGeometry
|
||||
|
||||
@@ -53,6 +53,13 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
|
||||
int32 MaxGenerationWorkerCores = 0;
|
||||
|
||||
// Keep this many task slots available for collision-critical, edited, and level-0 terrain while
|
||||
// coarse horizon work is in flight. A positive reserve prevents a full far-field queue from
|
||||
// delaying a new near tile because running tasks cannot be preempted. The runtime clamps the
|
||||
// value below the effective task budget; 4 is the conservative default for the ship settings.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0", ClampMax = "16"))
|
||||
int32 NearTaskReserve = 4;
|
||||
|
||||
// Nombre max de meshes appliqués (upload GPU) par frame. Seuls les vrais applies
|
||||
// comptent (chunks vides/périmés se vident gratuitement). À 32³ chaque apply est léger
|
||||
// (~0.1 ms) — tunable en live sur l'asset : montez (8-16) si le remplissage traîne,
|
||||
@@ -148,8 +155,9 @@ public:
|
||||
|
||||
// Coarsest LOD level. 0 = full-res only (no clipmap). Each level up doubles tile size &
|
||||
// reach at ~constant cost — raise this to see much farther for cheap. (Level L step = 2^L.)
|
||||
// Ship default: level 5, the measured horizon-distance balance at 768 chunks.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0", ClampMax = "8"))
|
||||
int32 MaxClipLevel = 4;
|
||||
int32 MaxClipLevel = 5;
|
||||
|
||||
// How many near levels mesh at FULL resolution (CHUNK_SIZE cells). Levels at or above this
|
||||
// use the cheaper CoarseTileCells → MUCH faster gen for the far field (it's far, so the
|
||||
@@ -171,9 +179,9 @@ public:
|
||||
// cell must fit inside a strate band — level ≥7 blanks via the too-coarse skip) and buy the
|
||||
// remaining horizon here. Cost: the extra ring is all same-level tiles — tile/draw/gen count
|
||||
// grows with (distance / 2^MaxClipLevel)², so each MaxClipLevel step down quadruples the ring.
|
||||
// 0 = off (natural reach, byte-identical streaming).
|
||||
// Ship default: 768 chunks (about 6.1 km at 8 m/chunk). 0 = off (natural reach).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
|
||||
int32 RenderDistanceChunks = 0;
|
||||
int32 RenderDistanceChunks = 768;
|
||||
|
||||
// F18 — the render-distance ring streams per-surface SHEETS instead of MC tiles: in an open
|
||||
// strate the far field is exactly two heightfields (TerrainZ + sky-cap CeilSurf, both already
|
||||
@@ -182,10 +190,11 @@ public:
|
||||
// sheet spans 2^FarSheetSpanLevels MC-tile footprints per axis). Non-open strates produce
|
||||
// empty sheets (their far ring was enclosed rock anyway). Carved features (passages, chasms,
|
||||
// spine) don't show at sheet distance. Needs the strate band armed (StrateContentCutMinLevel
|
||||
// active); in the inter-strate gap the sheet ring blanks until you land. Off = the ring stays
|
||||
// MC tiles at level MaxClipLevel (pre-F18 behaviour). Only matters when RenderDistanceChunks > 0.
|
||||
// active); in the inter-strate gap the sheet ring blanks until you land. The ship default is OFF:
|
||||
// the ring stays real MC tiles at level MaxClipLevel (pre-F18 behaviour). Only matters when
|
||||
// RenderDistanceChunks > 0.
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
|
||||
bool bFarSheetRing = true;
|
||||
bool bFarSheetRing = false;
|
||||
|
||||
// Sheet tile size = MaxClipLevel + this many levels (2 → one sheet covers 4×4 MC-tile
|
||||
// footprints → ~16× fewer far components). Sampling density stays that of the MaxClipLevel
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace VoxelForgeStartupTrace
|
||||
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path)
|
||||
bool bSealedSolidProof = false;
|
||||
bool bEmpty = true;
|
||||
int32 Vertices = 0;
|
||||
int32 Triangles = 0;
|
||||
uint64 GeometryBytes = 0;
|
||||
// This is worker-result -> game-thread mesh submission. Collision readiness is a later
|
||||
// per-tile RMC completion event recorded separately as collision_ready.
|
||||
double RequestToApplySeconds = 0.0;
|
||||
|
||||
@@ -32,6 +32,7 @@ class UVolumeTexture;
|
||||
class UMaterialInterface;
|
||||
class UMaterialInstanceDynamic;
|
||||
class FScopedGenerationPause;
|
||||
class ACameraActor;
|
||||
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
|
||||
|
||||
/** Per-request cancellation state. The game thread flips the flag when a pending tile leaves the
|
||||
@@ -87,6 +88,14 @@ struct FVoxelStreamingAnchor
|
||||
* - BeginPlay: Initialize generator, mesher
|
||||
* - Tick: Update chunks around player position
|
||||
*/
|
||||
enum class EVoxelTileWorkPriority : uint8
|
||||
{
|
||||
Horizon = 0,
|
||||
LOD0 = 1,
|
||||
Edited = 2,
|
||||
CollisionCritical = 3
|
||||
};
|
||||
|
||||
struct FChunkResult
|
||||
{
|
||||
FVoxelTileKey Tile; // which clipmap tile this mesh is for (carries coord + level)
|
||||
@@ -107,7 +116,10 @@ struct FChunkResult
|
||||
uint64 GenerationStartCycles = 0;
|
||||
uint64 GenerationEndCycles = 0;
|
||||
uint64 ApplyStartCycles = 0;
|
||||
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0;
|
||||
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
|
||||
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0;
|
||||
int32 NumVertices = 0;
|
||||
uint64 GeometryBytes = 0;
|
||||
bool bSealedSolidProof = false; // true when the cheap gap/seal proof skipped meshing
|
||||
bool bOutOfLayoutAirProof = false; // true when the exact constant-air guard skipped meshing
|
||||
// Clean, low-overhead work counters. These are carried with the result so the worker can
|
||||
@@ -117,6 +129,7 @@ struct FChunkResult
|
||||
bool bAborted = false; // Worker observed shutdown or cancellation; never mark this tile loaded
|
||||
bool bObsolete = false; // Worker observed per-tile desired-set cancellation
|
||||
uint32 DesiredEpoch = 0; // Desired-set epoch captured at request time
|
||||
uint32 ModificationEpoch = 0; // Diff-layer revision captured at request time
|
||||
bool bRecordStreamingLatency = true;
|
||||
bool bEmpty = true; // true ⇒ no surface (uniform all-air/all-solid or empty band); still loaded
|
||||
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
|
||||
@@ -153,6 +166,14 @@ struct FVoxelTileHashDumpRecord
|
||||
FString GeometryHash;
|
||||
};
|
||||
|
||||
/** Resident CPU geometry payload associated with one currently loaded tile. */
|
||||
struct FLoadedTileGeometryStats
|
||||
{
|
||||
uint64 Vertices = 0;
|
||||
uint64 Triangles = 0;
|
||||
uint64 GeometryBytes = 0;
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
class VOXELFORGE_API AVoxelWorld : public AActor
|
||||
{
|
||||
@@ -302,6 +323,8 @@ public:
|
||||
* capped to (logical cores − 2) so small CPUs don't thrash on a flat 16 (background
|
||||
* priority stops frame starvation, not the context-switch overhead). */
|
||||
int32 GetMaxConcurrentTasks() const;
|
||||
/** Reserve task slots for collision-critical/edit/LOD0 work; far work uses the remainder. */
|
||||
int32 GetNearTaskReserve(int32 MaxTasks) const;
|
||||
|
||||
//=========================================================================
|
||||
// TERRAIN MODIFICATION (player carving & filling)
|
||||
@@ -525,6 +548,10 @@ private:
|
||||
void AdvanceHeadlessCollisionGateStressTest(FVector& InOutPlayerPosition,
|
||||
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
||||
void ObserveHeadlessCollisionGateStressTest(const FVector& PlayerPosition, APawn* PlayerPawn);
|
||||
bool MeasureHeadlessCollisionGateStressTerrain(
|
||||
const FVector& WorldPosition, float CapsuleHalfHeightCm,
|
||||
float& OutFeetDensity, float& OutCenterDensity,
|
||||
float& OutSurfaceLocalVoxelZ, float& OutClearanceCm) const;
|
||||
void AdvanceHeadlessSurfaceFallTest(FVector& InOutPlayerPosition,
|
||||
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
||||
void InitializeHeadlessStrateCrossingTest();
|
||||
@@ -535,6 +562,9 @@ private:
|
||||
void MaybeFinishHeadlessSurfaceFallTest();
|
||||
void MaybeFinishHeadlessCollisionGateStressTest();
|
||||
void MaybeFinishHeadlessStreamingTest();
|
||||
void UpdateVisualStalenessAudit();
|
||||
void LogVisualStalenessAudit();
|
||||
void LogReadyTransitionVisibilityMeasure();
|
||||
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
||||
bool bSheetTile, const FVoxelMeshData& MeshData);
|
||||
@@ -727,7 +757,8 @@ public:
|
||||
*
|
||||
* @param ChunkCoord - Which chunk to load
|
||||
*/
|
||||
void LoadTile(const FVoxelTileKey& Tile, bool bHighPriority = false);
|
||||
void LoadTile(const FVoxelTileKey& Tile,
|
||||
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon);
|
||||
|
||||
/**
|
||||
* Worker-side gen for one tile: classify → GenerateMesh/GenerateSheetMesh → BuildTileStreamSet.
|
||||
@@ -860,6 +891,7 @@ public:
|
||||
void RecordLOD0ReadySample(uint64 RequestStartCycles, uint64 GenerationStartCycles,
|
||||
uint64 GenerationEndCycles, uint64 ApplyStartCycles,
|
||||
uint64 ReadyCycles, uint64 CollisionSubmittedCycles);
|
||||
void RecordLOD0RingReadyIfComplete();
|
||||
void LogStreamingLatencySummary() const;
|
||||
|
||||
// (GetLODForChunk / LODToStep / IsChunkInRange removed — dead since the clipmap
|
||||
@@ -873,6 +905,12 @@ public:
|
||||
// mode is NOT safe for multiple producers — concurrent Enqueues race on the tail
|
||||
// link and silently drop results, which leaks PendingChunkCoord slots until the
|
||||
// budget is exhausted and streaming stalls permanently. Mpsc guards the producer side.
|
||||
// Result queues mirror the scheduling order: collision support first, then edited tiles,
|
||||
// then ordinary LOD0, then the horizon. This keeps a support result from waiting behind a
|
||||
// completed near-field backlog while preserving one consumer on the game thread.
|
||||
TQueue<FChunkResult, EQueueMode::Mpsc> CriticalProcessQueue;
|
||||
TQueue<FChunkResult, EQueueMode::Mpsc> EditedProcessQueue;
|
||||
TQueue<FChunkResult, EQueueMode::Mpsc> NearProcessQueue;
|
||||
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
||||
TSet<FVoxelTileKey> PendingTiles; // tiles with a gen task in flight
|
||||
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
|
||||
@@ -888,12 +926,11 @@ public:
|
||||
int32 MeshBandChunkHi = MAX_int32;
|
||||
TSet<FVoxelTileKey> BandRemeshQueue;
|
||||
|
||||
// DIG RESPONSIVENESS — loaded level-0 tiles touched by a player carve/fill that still need an
|
||||
// async re-mesh (the neighbours of the synchronously-remeshed centre tile, plus any tile that was
|
||||
// mid-gen at carve time). Drained FIRST in the submit loop (ahead of streaming + band) and launched
|
||||
// at BackgroundHigh so a dig never waits behind streaming. Unlike the old inline RemeshDirtyChunks,
|
||||
// a tile that's in flight is KEPT queued (not dropped) so the stale pre-carve result is corrected
|
||||
// once it lands — the source of "the hole shows up a beat late, or not until I move".
|
||||
// DIG RESPONSIVENESS — tiles at every LOD touched by a player carve/fill that still need an
|
||||
// async re-mesh (the synchronously-remeshed centre tile is excluded). Drained FIRST in the submit
|
||||
// loop (ahead of streaming + band) and launched at BackgroundHigh so a dig never waits behind
|
||||
// streaming. A tile that's in flight is marked obsolete and KEPT queued so a stale pre-carve
|
||||
// result cannot publish and the post-carve result is admitted immediately.
|
||||
TSet<FVoxelTileKey> DirtyRemeshQueue;
|
||||
|
||||
// F18 — TROU XY de l'anneau feuille (voxels ; Max EXCLUSIF ; sentinelles MAX/MIN = pas de
|
||||
@@ -957,10 +994,30 @@ public:
|
||||
bool bSampleStacksStarted = false;
|
||||
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
||||
// index to test transition coverage without rescanning the full desired set per old tile.
|
||||
TMap<FVoxelTileKey, TArray<FVoxelTileKey>> DesiredTransitionDescendants;
|
||||
int32 DesiredTransitionMaxLevel = 0;
|
||||
uint32 DesiredEpoch = 0; // increments whenever DesiredStamped is rebuilt
|
||||
int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count
|
||||
float PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
||||
|
||||
struct FReadyTransitionVisibilitySample
|
||||
{
|
||||
uint64 Cycles = 0;
|
||||
int32 TileComponentCount = 0;
|
||||
int32 DesiredTileCount = 0;
|
||||
};
|
||||
struct FFarFieldSubmitSample
|
||||
{
|
||||
uint64 Cycles = 0;
|
||||
int32 DesiredTileCount = 0;
|
||||
int32 MaxDesiredLevel = 0;
|
||||
};
|
||||
bool bMeasureReadyTransitionVisibility = false;
|
||||
TArray<FReadyTransitionVisibilitySample> ReadyTransitionVisibilitySamples;
|
||||
TArray<FFarFieldSubmitSample> FarFieldSubmitSamples;
|
||||
|
||||
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level; sheet tiles
|
||||
// above the configured MC range never enter the outer classifier.
|
||||
static constexpr int32 TrackedClassifierLODCount = 9;
|
||||
@@ -984,6 +1041,22 @@ public:
|
||||
uint64 AppliedVisibleTileCountByLevel[TrackedClassifierLODCount]{};
|
||||
uint64 AppliedTriangleCountByLevel[TrackedClassifierLODCount]{};
|
||||
uint64 AppliedBandTileCountByLevel[TrackedClassifierLODCount]{};
|
||||
TMap<FVoxelTileKey, FLoadedTileGeometryStats> LoadedTileGeometry;
|
||||
|
||||
// Diff revision captured by tile requests. A result from before an edit is never allowed to
|
||||
// replace a tile whose footprint intersects that edit; the old visual/collision state remains
|
||||
// until a post-edit result is applied.
|
||||
struct FModificationRevision
|
||||
{
|
||||
uint32 Epoch = 0;
|
||||
FVector MinVoxel = FVector::ZeroVector;
|
||||
FVector MaxVoxel = FVector::ZeroVector;
|
||||
};
|
||||
uint32 ModificationEpoch = 0;
|
||||
FVector LastModificationMinVoxel = FVector::ZeroVector;
|
||||
FVector LastModificationMaxVoxel = FVector::ZeroVector;
|
||||
TArray<FModificationRevision> ModificationRevisions;
|
||||
TMap<FVoxelTileKey, uint32> AppliedModificationEpoch;
|
||||
|
||||
struct FStreamingLatencySample
|
||||
{
|
||||
@@ -993,10 +1066,18 @@ public:
|
||||
double CollisionCookSeconds = 0.0;
|
||||
};
|
||||
TArray<FStreamingLatencySample> LOD0ReadySamples;
|
||||
// Trace-only ring timing: each desired-set rebuild starts a new epoch. A ring is complete
|
||||
// when every current desired level-0 key has reached LoadedTiles and no level-0 request for
|
||||
// that epoch remains pending. This measures visible near-field readiness independently of the
|
||||
// much larger horizon settle point.
|
||||
uint64 DesiredEpochStartCycles = 0;
|
||||
FIntVector DesiredEpochCenterChunk = FIntVector::ZeroValue;
|
||||
uint32 LastLOD0RingReadyEpoch = MAX_uint32;
|
||||
|
||||
// Headless validation hook. These are intentionally non-UPROPERTY state and are enabled only
|
||||
// by explicit command-line switches; they do not alter ordinary player movement or streaming.
|
||||
bool bHeadlessStreamingTestMovement = false;
|
||||
bool bHeadlessStreamingTestExitOnSteady = false;
|
||||
bool bHeadlessStreamingTestExitRequested = false;
|
||||
bool bStartupTraceThroughCrossing = false;
|
||||
bool bHeadlessStreamingTestCenterOverride = false;
|
||||
@@ -1011,6 +1092,16 @@ public:
|
||||
double HeadlessStreamingTestBeginSeconds = 0.0;
|
||||
double HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
||||
|
||||
// Transient owner-view render hook. Enabled only by explicit voxel.TestCeilingView arguments;
|
||||
// the camera and smaller test radius are held in memory and never saved to a package.
|
||||
bool bTestCeilingViewEnabled = false;
|
||||
bool bTestCeilingCaptureRequested = false;
|
||||
TWeakObjectPtr<ACameraActor> TestCeilingCamera;
|
||||
FVector TestCeilingCameraWorldPosition = FVector::ZeroVector;
|
||||
FRotator TestCeilingCameraRotation = FRotator::ZeroRotator;
|
||||
double TestCeilingViewBeginSeconds = 0.0;
|
||||
float TestCeilingCaptureDelaySeconds = 18.0f;
|
||||
|
||||
// Explicit high-speed collision-gate repro. The streamer stays centred at the launch tile while
|
||||
// the character uses ordinary swept movement across the still-pending desired set. The test
|
||||
// distinguishes a gate hold from a full horizontal stop so a fall-only gate can be validated.
|
||||
@@ -1022,22 +1113,39 @@ public:
|
||||
bool bHeadlessCollisionGateStressTestExitRequested = false;
|
||||
bool bHeadlessCollisionGateStressTestGateWasEngaged = false;
|
||||
bool bHeadlessCollisionGateStressTestFullStop = false;
|
||||
bool bHeadlessCollisionGateStressTestStartHeightCrossed = false;
|
||||
bool bHeadlessCollisionGateStressTestTerrainViolation = false;
|
||||
bool bHeadlessCollisionGateStressTestTerrainMeasurementMissing = false;
|
||||
bool bHeadlessCollisionGateStressTestLanded = false;
|
||||
bool bHeadlessCollisionGateStressTestLaunchSupportTimingCaptured = false;
|
||||
float HeadlessCollisionGateStressTestSpeedCmPerSecond = 6000.0f;
|
||||
float HeadlessCollisionGateStressTestStartDelaySeconds = 0.1f;
|
||||
float HeadlessCollisionGateStressTestDurationSeconds = 2.0f;
|
||||
float HeadlessCollisionGateStressTestTimeoutSeconds = 15.0f;
|
||||
double HeadlessCollisionGateStressTestBeginSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestStartSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestLandedSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestGateStartSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestFullStopStartSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestDistanceCm = 0.0;
|
||||
double HeadlessCollisionGateStressTestGateDistanceCm = 0.0;
|
||||
double HeadlessCollisionGateStressTestTotalGateDurationSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestMaxGateDurationSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestFullStopDurationSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestLaunchSupportCheckSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestLaunchSupportSubmittedSeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestLaunchSupportReadySeconds = 0.0;
|
||||
double HeadlessCollisionGateStressTestMinClearanceCm = 1.0e30;
|
||||
float HeadlessCollisionGateStressTestMinFeetDensity = 0.0f;
|
||||
float HeadlessCollisionGateStressTestMinSurfaceLocalVoxelZ = 0.0f;
|
||||
int32 HeadlessCollisionGateStressTestTerrainSampleCount = 0;
|
||||
int32 HeadlessCollisionGateStressTestTraceFrame = 0;
|
||||
int32 HeadlessCollisionGateStressTestGateHoldCount = 0;
|
||||
int32 HeadlessCollisionGateStressTestFullStopCount = 0;
|
||||
FVector HeadlessCollisionGateStressTestDirection = FVector::XAxisVector;
|
||||
FVector HeadlessCollisionGateStressTestLastActualPosition = FVector::ZeroVector;
|
||||
FVector HeadlessCollisionGateStressTestStartPosition = FVector::ZeroVector;
|
||||
FVector HeadlessCollisionGateStressTestMinClearancePosition = FVector::ZeroVector;
|
||||
FVoxelTileKey HeadlessCollisionGateStressTestLaunchSupportTile;
|
||||
FString HeadlessCollisionGateStressTestFailureReason;
|
||||
|
||||
// Explicit owner-layout collision-gate repro. This is a command-line-only harness path: it
|
||||
@@ -1105,6 +1213,24 @@ public:
|
||||
FString HeadlessStrateCrossingTestFailureReason;
|
||||
TArray<FVector> HeadlessStrateCrossingTestRoute;
|
||||
|
||||
// Explicit visual-staleness evidence. Mode 1 audits an edit; mode 2 audits an LOD transition.
|
||||
// These fields are command-line-only diagnostics and are inert in ordinary gameplay.
|
||||
bool bVisualStalenessAudit = false;
|
||||
int32 VisualStalenessAuditMode = 0;
|
||||
bool bVisualStalenessAuditExitLogged = false;
|
||||
uint32 VisualAuditEditEpoch = 0;
|
||||
FVector VisualAuditEditMinVoxel = FVector::ZeroVector;
|
||||
FVector VisualAuditEditMaxVoxel = FVector::ZeroVector;
|
||||
double VisualAuditEditBeginSeconds = 0.0;
|
||||
double VisualAuditEditStaleDurationSeconds = -1.0;
|
||||
int32 VisualAuditEditMaxStaleTiles = 0;
|
||||
bool bVisualAuditEditSawStale = false;
|
||||
bool bVisualAuditEditResolved = false;
|
||||
double VisualAuditLODOverlapStartSeconds = 0.0;
|
||||
double VisualAuditLODOverlapMaxSeconds = 0.0;
|
||||
int32 VisualAuditLODOverlapEvents = 0;
|
||||
int32 VisualAuditLODOverlapMaxPairs = 0;
|
||||
|
||||
// PLAYER COLLISION GATE — only the downward component is held while the level-0 collision body
|
||||
// beneath the pawn's feet is unresolved. Horizontal movement and player control remain live.
|
||||
TWeakObjectPtr<APawn> PawnTickPrerequisite;
|
||||
@@ -1121,6 +1247,14 @@ public:
|
||||
bool bPawnGateUnsupportedReported = false;
|
||||
FVoxelTileKey PawnGateRequestedSupportTile;
|
||||
bool bPawnGateSupportRequestReported = false;
|
||||
// Keep the first unresolved support request's timing for the startup gate repro. This is
|
||||
// intentionally retained after release so the stress-test result can report the bootstrap path.
|
||||
FVoxelTileKey StartupCollisionGateSupportTile;
|
||||
bool bStartupCollisionGateTimingCaptured = false;
|
||||
bool bStartupCollisionGateAdoptionReported = false;
|
||||
double StartupCollisionGateFirstCheckSeconds = 0.0;
|
||||
double StartupCollisionGateSubmittedSeconds = 0.0;
|
||||
double StartupCollisionGateReadySeconds = 0.0;
|
||||
|
||||
// Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée.
|
||||
// BuildDesiredTiles upserte le stamp courant puis balaie la map UNE fois : les entrées à stamp
|
||||
@@ -1159,6 +1293,9 @@ public:
|
||||
|
||||
void ProcessPendingChunks();
|
||||
void ProcessUnloadQueue(); // budgeted teardown drain (see PendingUnload)
|
||||
void QueuePendingUnload(const FVoxelTileKey& Tile);
|
||||
void HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile);
|
||||
void ReconcileReadyTransitionVisibility();
|
||||
|
||||
/**
|
||||
* Re-queue loaded chunks for async re-generation + re-meshing.
|
||||
|
||||
+472
-20
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity')]
|
||||
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism')]
|
||||
[string]$Scenario = 'canonical',
|
||||
[switch]$Build,
|
||||
[ValidateSet('owner', 'default')]
|
||||
@@ -8,7 +8,12 @@ param(
|
||||
[hashtable]$Cvars = @{},
|
||||
[string]$TestFilter,
|
||||
[string]$Out,
|
||||
[string]$Label
|
||||
[string]$Label,
|
||||
[ValidateRange(0, 8)]
|
||||
[int]$HorizonLevel = 5,
|
||||
[ValidateRange(1, 100000)]
|
||||
[int]$HorizonRenderDistanceChunks = 768,
|
||||
[string]$SharedDDC
|
||||
)
|
||||
|
||||
# One guarded entry point for all VoxelForge measurements. It intentionally keeps the Unreal
|
||||
@@ -28,6 +33,7 @@ $EngineRoot = 'E:\Program Files\Epic Games\UE_5.7'
|
||||
$EditorExe = Join-Path $EngineRoot 'Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
$UbtDll = Join-Path $EngineRoot 'Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll'
|
||||
$LaunchTimeoutSeconds = 1800
|
||||
$UnrealPrivateMemoryLimitBytes = 8GB
|
||||
# The VoxelForge category includes two late, several-minute op-stack sweeps. They remain in
|
||||
# the suite; when the serialized aggregate launch reaches this 30-minute guard, validate those
|
||||
# named tests with -TestFilter so every test still runs under the standing launch limit.
|
||||
@@ -43,12 +49,25 @@ $BuildRecord = [ordered]@{ requested = [bool]$Build; status = 'not_requested' }
|
||||
$AssetEvidence = @()
|
||||
$CommandletMetrics = [ordered]@{}
|
||||
$GameMetrics = [ordered]@{}
|
||||
$HorizonMetrics = [ordered]@{
|
||||
requested = $Scenario -in @('horizon', 'horizon-render')
|
||||
status = 'not_run'
|
||||
level = $HorizonLevel
|
||||
render_distance_chunks = $HorizonRenderDistanceChunks
|
||||
far_sheet_ring = $false
|
||||
}
|
||||
$HorizonRenderMetrics = [ordered]@{ requested = $Scenario -eq 'horizon-render'; status = 'not_run' }
|
||||
$DeterminismEvidence = [ordered]@{ requested = $Scenario -eq 'determinism'; status = 'not_run' }
|
||||
$SurfaceFallMetrics = [ordered]@{
|
||||
requested = $Scenario -eq 'surface-fall' -or
|
||||
($Scenario -eq 'tests' -and [string]::IsNullOrWhiteSpace($TestFilter))
|
||||
status = 'not_run'
|
||||
}
|
||||
$StrateCrossingMetrics = [ordered]@{ requested = $Scenario -eq 'crossing'; status = 'not_run' }
|
||||
$VisualStalenessMetrics = [ordered]@{
|
||||
requested = $Scenario -in @('visual-edit', 'visual-lod')
|
||||
status = 'not_run'
|
||||
}
|
||||
$CollisionGateStressMetrics = [ordered]@{
|
||||
requested = $Scenario -eq 'gate-stress' -or
|
||||
($Scenario -eq 'tests' -and [string]::IsNullOrWhiteSpace($TestFilter))
|
||||
@@ -65,6 +84,7 @@ $LocalAppDataRoot = Join-Path ([Environment]::GetFolderPath('LocalApplicationDat
|
||||
$RunRoot = $null
|
||||
$ResultPath = $null
|
||||
$SummaryPath = $null
|
||||
$DDCPath = $null
|
||||
|
||||
function Add-Failure([string]$Reason) {
|
||||
if ([string]::IsNullOrWhiteSpace($Reason)) { return }
|
||||
@@ -264,17 +284,48 @@ function Get-NewCrashes($Before, [datetime]$StartedUtc) {
|
||||
return @($new)
|
||||
}
|
||||
|
||||
function Wait-OwnedProcess($Process, [int]$TimeoutSeconds) {
|
||||
$finished = $Process.WaitForExit($TimeoutSeconds * 1000)
|
||||
if ($finished) {
|
||||
function Wait-OwnedProcess($Process, [int]$TimeoutSeconds, [switch]$EnforceMemoryLimit) {
|
||||
$watch = [Diagnostics.Stopwatch]::StartNew()
|
||||
$peakPrivateBytes = 0L
|
||||
while ($watch.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
|
||||
if ($Process.HasExited) {
|
||||
[void]$Process.WaitForExit()
|
||||
return [ordered]@{ timed_out = $false; exit_code = [int]$Process.ExitCode }
|
||||
return [ordered]@{
|
||||
timed_out = $false
|
||||
memory_limit_hit = $false
|
||||
peak_private_bytes = $peakPrivateBytes
|
||||
exit_code = [int]$Process.ExitCode
|
||||
}
|
||||
}
|
||||
if ($EnforceMemoryLimit) {
|
||||
$owned = Get-Process -Id ([int]$Process.Id) -ErrorAction SilentlyContinue
|
||||
if ($null -ne $owned) {
|
||||
$peakPrivateBytes = [Math]::Max($peakPrivateBytes, [int64]$owned.PrivateMemorySize64)
|
||||
if ([int64]$owned.PrivateMemorySize64 -gt $UnrealPrivateMemoryLimitBytes) {
|
||||
# This PID came from our Start-Process call. Do not use a name-wide kill here.
|
||||
try { Stop-Process -Id ([int]$Process.Id) -Force -ErrorAction Stop } catch { }
|
||||
try { [void]$Process.WaitForExit(5000) } catch { }
|
||||
return [ordered]@{
|
||||
timed_out = $false
|
||||
memory_limit_hit = $true
|
||||
peak_private_bytes = $peakPrivateBytes
|
||||
exit_code = $null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Start-Sleep -Milliseconds 1000
|
||||
}
|
||||
# This PID came from our Start-Process call. Never use a name-based kill or a broad process
|
||||
# query here: an owner's editor may be unrelated and must remain untouched.
|
||||
try { Stop-Process -Id ([int]$Process.Id) -Force -ErrorAction Stop } catch { }
|
||||
try { [void]$Process.WaitForExit(5000) } catch { }
|
||||
return [ordered]@{ timed_out = $true; exit_code = $null }
|
||||
return [ordered]@{
|
||||
timed_out = $true
|
||||
memory_limit_hit = $false
|
||||
peak_private_bytes = $peakPrivateBytes
|
||||
exit_code = $null
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPath,
|
||||
@@ -293,6 +344,8 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
|
||||
ended_utc = $null
|
||||
exit_code = $null
|
||||
timed_out = $false
|
||||
memory_limit_hit = $false
|
||||
peak_private_bytes = 0L
|
||||
crash_reporters_reaped = @()
|
||||
new_crashes = @()
|
||||
log = $LogPath
|
||||
@@ -309,12 +362,22 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
|
||||
# -LocalDataCachePath are supplied. Keep this child process inside the run directory.
|
||||
$env:LOCALAPPDATA = $isolatedLocalAppData
|
||||
$process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot `
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle Hidden -PassThru
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle Hidden `
|
||||
-PassThru
|
||||
try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal }
|
||||
catch {
|
||||
try { Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop } catch { }
|
||||
throw "Could not lower owned Unreal PID $($process.Id) to BelowNormal: $($_.Exception.Message)"
|
||||
}
|
||||
$ledger.pid = [int]$process.Id
|
||||
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds
|
||||
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds -EnforceMemoryLimit
|
||||
$ledger.timed_out = [bool]$wait.timed_out
|
||||
$ledger.memory_limit_hit = [bool]$wait.memory_limit_hit
|
||||
$ledger.peak_private_bytes = [int64]$wait.peak_private_bytes
|
||||
$ledger.exit_code = $wait.exit_code
|
||||
$ledger.status = if ($ledger.timed_out) { 'timeout' } elseif ($ledger.exit_code -eq 0) { 'passed' } else { 'failed' }
|
||||
$ledger.status = if ($ledger.timed_out) { 'timeout' }
|
||||
elseif ($ledger.memory_limit_hit) { 'memory_limit' }
|
||||
elseif ($ledger.exit_code -eq 0) { 'passed' } else { 'failed' }
|
||||
} catch {
|
||||
$ledger.status = 'launch_error'
|
||||
$ledger.error = $_.Exception.Message
|
||||
@@ -356,7 +419,13 @@ function Invoke-BuildProcess([string[]]$Arguments, [string]$LogPath, [string]$Er
|
||||
$env:DOTNET_NOLOGO = '1'
|
||||
$env:LOCALAPPDATA = $isolatedLocalAppData
|
||||
$process = Start-Process -FilePath $dotnet -ArgumentList $Arguments -WorkingDirectory $PluginRoot `
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $ErrorPath -WindowStyle Hidden -PassThru
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $ErrorPath -WindowStyle Hidden `
|
||||
-PassThru
|
||||
try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal }
|
||||
catch {
|
||||
try { Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop } catch { }
|
||||
throw "Could not lower owned build PID $($process.Id) to BelowNormal: $($_.Exception.Message)"
|
||||
}
|
||||
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds
|
||||
} finally {
|
||||
if ($null -eq $oldDotnetCliHome) { Remove-Item Env:DOTNET_CLI_HOME -ErrorAction SilentlyContinue } else { $env:DOTNET_CLI_HOME = $oldDotnetCliHome }
|
||||
@@ -435,7 +504,7 @@ function Sync-OwnerAssets {
|
||||
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
|
||||
[void]$rows.Add([ordered]@{
|
||||
name = $assetName
|
||||
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress')
|
||||
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'horizon', 'horizon-render', 'determinism')
|
||||
source_path = $source
|
||||
source_sha256 = Get-Sha256 $source
|
||||
staged_path = $target
|
||||
@@ -483,7 +552,7 @@ function Get-AssetEvidence {
|
||||
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
|
||||
[void]$rows.Add([ordered]@{
|
||||
name = $assetName
|
||||
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress')
|
||||
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'horizon', 'horizon-render', 'determinism')
|
||||
source_path = $source
|
||||
source_sha256 = Get-Sha256 $source
|
||||
staged_path = $target
|
||||
@@ -508,13 +577,19 @@ function Merge-Cvars([System.Collections.IDictionary]$Base) {
|
||||
return $merged
|
||||
}
|
||||
|
||||
function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap) {
|
||||
function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap, [bool]$EnableRender = $false) {
|
||||
$safeLabel = ($Label -replace '[^A-Za-z0-9_.-]', '_')
|
||||
$args = [System.Collections.Generic.List[string]]::new()
|
||||
# Quoting the whole -project= token is intentional: this is the failure mode that used to
|
||||
# swallow every argument after "Projet Unreal".
|
||||
[void]$args.Add(('-project="{0}"' -f $HostProject))
|
||||
if ($EnableRender) {
|
||||
[void]$args.Add('-RenderOffscreen')
|
||||
[void]$args.Add('-ResX=1280')
|
||||
[void]$args.Add('-ResY=720')
|
||||
} else {
|
||||
[void]$args.Add('-nullrhi')
|
||||
}
|
||||
[void]$args.Add('-DDC-ForceMemoryCache')
|
||||
[void]$args.Add('-unattended')
|
||||
[void]$args.Add('-nop4')
|
||||
@@ -524,13 +599,14 @@ function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap) {
|
||||
[void]$args.Add('-FullStdOutLogOutput')
|
||||
[void]$args.Add('-forcelogflush')
|
||||
[void]$args.Add('-NoZenAutoLaunch')
|
||||
[void]$args.Add('-notraceserver')
|
||||
[void]$args.Add('-nocrashreports')
|
||||
[void]$args.Add('-WindowsPlatformCrashContext.ForceCrashReportDialogOff=1')
|
||||
[void]$args.Add("-enginesaveddirsuffix=VoxelForgeTest_$safeLabel")
|
||||
[void]$args.Add(('-userdir="{0}"' -f (Join-Path $RunRoot 'User')))
|
||||
[void]$args.Add(('-ZenDataPath="{0}"' -f (Join-Path $RunRoot 'ZenData')))
|
||||
[void]$args.Add('-ddc=NoZenLocalFallback')
|
||||
[void]$args.Add(('-LocalDataCachePath="{0}"' -f (Join-Path $RunRoot 'DDC')))
|
||||
[void]$args.Add(('-LocalDataCachePath="{0}"' -f $DDCPath))
|
||||
[void]$args.Add(('-abslog="{0}"' -f $LogPath))
|
||||
if ($Kind -eq 'commandlet') {
|
||||
[void]$args.Add('-run=VoxelForgeExplore')
|
||||
@@ -542,7 +618,7 @@ function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap) {
|
||||
$switch = [string]$key
|
||||
if (-not $switch.StartsWith('-')) { $switch = "-$switch" }
|
||||
$value = Get-ValueText $CvarMap[$key]
|
||||
if ($value -match '\s') {
|
||||
if ($value -match '\s' -or $value.Contains(',')) {
|
||||
[void]$args.Add(('{0}="{1}"' -f $switch, $value))
|
||||
} else {
|
||||
[void]$args.Add("$switch=$value")
|
||||
@@ -670,6 +746,212 @@ function Get-StreamingMetrics([string]$LogPath) {
|
||||
return $metrics
|
||||
}
|
||||
|
||||
function Get-HorizonMetrics([string]$TracePath, $Ledger, $LatencyMetrics) {
|
||||
$metrics = [ordered]@{
|
||||
status = 'missing'
|
||||
trace = $TracePath
|
||||
desired_tiles_by_level = [ordered]@{}
|
||||
loaded_tiles_by_level = [ordered]@{}
|
||||
vertices_by_level = [ordered]@{}
|
||||
triangles_by_level = [ordered]@{}
|
||||
geometry_memory_bytes_by_level = [ordered]@{}
|
||||
geometry_memory_mib_by_level = [ordered]@{}
|
||||
desired_tiles_total = 0
|
||||
loaded_tiles_total = 0
|
||||
vertices_total = 0
|
||||
triangles_total = 0
|
||||
geometry_memory_bytes_total = 0
|
||||
geometry_memory_mib_total = 0.0
|
||||
worker_generation_seconds = if ($null -ne $LatencyMetrics) {
|
||||
Get-JsonValue $LatencyMetrics 'worker_generation_sum_s' $null
|
||||
} else { $null }
|
||||
settled_trace_seconds = $null
|
||||
trace_summary_wall_seconds = $null
|
||||
process_wall_seconds = $null
|
||||
peak_private_bytes = if ($null -ne $Ledger) {
|
||||
[int64](Get-JsonValue $Ledger 'peak_private_bytes' 0)
|
||||
} else { 0L }
|
||||
peak_private_gib = 0.0
|
||||
trace_records_seen = $null
|
||||
trace_records_written = $null
|
||||
trace_records_suppressed = $null
|
||||
}
|
||||
$metrics.peak_private_gib = [Math]::Round($metrics.peak_private_bytes / 1GB, 3)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $TracePath -PathType Leaf)) { return $metrics }
|
||||
$events = [System.Collections.Generic.List[object]]::new()
|
||||
foreach ($line in @(Get-Content -LiteralPath $TracePath -ErrorAction SilentlyContinue)) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||
try { [void]$events.Add(($line | ConvertFrom-Json -ErrorAction Stop)) } catch { }
|
||||
}
|
||||
$steady = @($events | Where-Object { [string](Get-JsonValue $_ 'type' '') -eq 'steady_state' } | Select-Object -Last 1)
|
||||
$summary = @($events | Where-Object { [string](Get-JsonValue $_ 'type' '') -eq 'summary' } | Select-Object -Last 1)
|
||||
$desiredEvents = @($events | Where-Object { [string](Get-JsonValue $_ 'type' '') -eq 'steady_desired_level' })
|
||||
$loadedEvents = @($events | Where-Object { [string](Get-JsonValue $_ 'type' '') -eq 'steady_loaded_level' })
|
||||
if ($steady.Count -eq 0 -or $summary.Count -eq 0 -or $desiredEvents.Count -eq 0) {
|
||||
return $metrics
|
||||
}
|
||||
|
||||
foreach ($event in $desiredEvents) {
|
||||
$level = [string][int](Get-JsonValue $event 'level' -1)
|
||||
$metrics.desired_tiles_by_level[$level] = [int](Get-JsonValue $event 'tiles' 0)
|
||||
}
|
||||
foreach ($event in $loadedEvents) {
|
||||
$level = [string][int](Get-JsonValue $event 'level' -1)
|
||||
$metrics.loaded_tiles_by_level[$level] = [int](Get-JsonValue $event 'tiles' 0)
|
||||
$metrics.vertices_by_level[$level] = [int64](Get-JsonValue $event 'vertices' 0)
|
||||
$metrics.triangles_by_level[$level] = [int64](Get-JsonValue $event 'triangles' 0)
|
||||
$bytes = [int64](Get-JsonValue $event 'geometry_bytes' 0)
|
||||
$metrics.geometry_memory_bytes_by_level[$level] = $bytes
|
||||
$metrics.geometry_memory_mib_by_level[$level] = [Math]::Round($bytes / 1MB, 3)
|
||||
}
|
||||
foreach ($value in $metrics.desired_tiles_by_level.Values) { $metrics.desired_tiles_total += [int64]$value }
|
||||
foreach ($value in $metrics.loaded_tiles_by_level.Values) { $metrics.loaded_tiles_total += [int64]$value }
|
||||
foreach ($value in $metrics.vertices_by_level.Values) { $metrics.vertices_total += [int64]$value }
|
||||
foreach ($value in $metrics.triangles_by_level.Values) { $metrics.triangles_total += [int64]$value }
|
||||
foreach ($value in $metrics.geometry_memory_bytes_by_level.Values) { $metrics.geometry_memory_bytes_total += [int64]$value }
|
||||
$metrics.geometry_memory_mib_total = [Math]::Round($metrics.geometry_memory_bytes_total / 1MB, 3)
|
||||
$metrics.settled_trace_seconds = [double](Get-JsonValue $steady[0] 't_s' 0.0)
|
||||
$metrics.trace_summary_wall_seconds = [double](Get-JsonValue $summary[0] 'wall_s' 0.0)
|
||||
$metrics.trace_records_seen = [int](Get-JsonValue $summary[0] 'tile_records_seen' 0)
|
||||
$metrics.trace_records_written = [int](Get-JsonValue $summary[0] 'tile_records_written' 0)
|
||||
$metrics.trace_records_suppressed = [int](Get-JsonValue $summary[0] 'tile_records_suppressed' 0)
|
||||
if ($null -ne $Ledger) {
|
||||
$started = [datetime]::Parse([string](Get-JsonValue $Ledger 'started_utc' ''))
|
||||
$ended = [datetime]::Parse([string](Get-JsonValue $Ledger 'ended_utc' ''))
|
||||
$metrics.process_wall_seconds = [Math]::Round(($ended.ToUniversalTime() - $started.ToUniversalTime()).TotalSeconds, 3)
|
||||
}
|
||||
$metrics.status = 'ok'
|
||||
return $metrics
|
||||
}
|
||||
|
||||
function Get-HorizonRenderMetrics($Ledger, [string]$LogPath) {
|
||||
$metrics = [ordered]@{
|
||||
status = 'failed'
|
||||
log = $LogPath
|
||||
capture_requested = $null
|
||||
capture_elapsed_seconds = $null
|
||||
capture_visible_tiles = $null
|
||||
capture_triangles = $null
|
||||
images = @()
|
||||
}
|
||||
$lines = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeTestCeiling\] capture_requested=' -ErrorAction SilentlyContinue)
|
||||
if ($lines.Count -gt 0) {
|
||||
$line = [string]$lines[-1].Line
|
||||
$metrics.raw = $line
|
||||
foreach ($key in @('capture_requested', 'elapsed_s', 'visible_tiles', 'triangles')) {
|
||||
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
|
||||
if ($match.Success) {
|
||||
if ($key -eq 'elapsed_s') { $metrics.capture_elapsed_seconds = [double]$match.Groups['v'].Value }
|
||||
elseif ($key -eq 'visible_tiles') { $metrics.capture_visible_tiles = [int64]$match.Groups['v'].Value }
|
||||
elseif ($key -eq 'triangles') { $metrics.capture_triangles = [int64]$match.Groups['v'].Value }
|
||||
else { $metrics.capture_requested = [int]$match.Groups['v'].Value }
|
||||
}
|
||||
}
|
||||
}
|
||||
$started = if ($null -ne $Ledger) {
|
||||
[datetime]::Parse([string](Get-JsonValue $Ledger 'started_utc' ''))
|
||||
} else { [datetime]::MinValue }
|
||||
$roots = @(
|
||||
(Join-Path $HostRoot 'Saved'),
|
||||
$RunRoot,
|
||||
(Join-Path $RunRoot 'User'))
|
||||
$images = [System.Collections.Generic.List[object]]::new()
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $root -PathType Container)) { continue }
|
||||
foreach ($image in @(Get-ChildItem -LiteralPath $root -Filter '*.png' -File -Recurse -ErrorAction SilentlyContinue)) {
|
||||
if ($image.LastWriteTimeUtc -lt $started.ToUniversalTime().AddSeconds(-2)) { continue }
|
||||
[void]$images.Add([ordered]@{
|
||||
path = $image.FullName
|
||||
bytes = [int64]$image.Length
|
||||
modified_utc = $image.LastWriteTimeUtc.ToString('o')
|
||||
})
|
||||
}
|
||||
}
|
||||
$metrics.images = @($images | Sort-Object path -Unique)
|
||||
if ($metrics.capture_requested -eq 1 -and $metrics.images.Count -gt 0) {
|
||||
$metrics.status = 'passed'
|
||||
} elseif ($null -eq $Ledger -or (Get-JsonValue $Ledger 'status' 'failed') -ne 'passed') {
|
||||
$metrics.status = 'not_run'
|
||||
}
|
||||
return $metrics
|
||||
}
|
||||
|
||||
function Compare-DeterminismDumps([string]$DefaultPath, [string]$CrtFma3OffPath) {
|
||||
$defaultJson = Read-JsonFile $DefaultPath
|
||||
$fmaJson = Read-JsonFile $CrtFma3OffPath
|
||||
$evidence = [ordered]@{
|
||||
status = 'failed'
|
||||
default_dump = $DefaultPath
|
||||
crt_fma3_off_dump = $CrtFma3OffPath
|
||||
default_record_count = 0
|
||||
crt_fma3_off_record_count = 0
|
||||
compared_tile_count = 0
|
||||
first_differences = @()
|
||||
}
|
||||
if ($null -eq $defaultJson -or $null -eq $fmaJson) {
|
||||
$evidence.reason = 'one or both tile hash dumps are missing'
|
||||
return $evidence
|
||||
}
|
||||
$defaultRecords = @((Get-JsonValue $defaultJson 'records' @()))
|
||||
$fmaRecords = @((Get-JsonValue $fmaJson 'records' @()))
|
||||
$evidence.default_record_count = $defaultRecords.Count
|
||||
$evidence.crt_fma3_off_record_count = $fmaRecords.Count
|
||||
$defaultByKey = @{}
|
||||
$fmaByKey = @{}
|
||||
foreach ($record in $defaultRecords) {
|
||||
$key = "$($record.tile_x):$($record.tile_y):$($record.tile_z):$($record.level)"
|
||||
$defaultByKey[$key] = $record
|
||||
}
|
||||
foreach ($record in $fmaRecords) {
|
||||
$key = "$($record.tile_x):$($record.tile_y):$($record.tile_z):$($record.level)"
|
||||
$fmaByKey[$key] = $record
|
||||
}
|
||||
$differences = [System.Collections.Generic.List[object]]::new()
|
||||
$keys = @($defaultByKey.Keys) + @($fmaByKey.Keys) | Sort-Object -Unique
|
||||
foreach ($key in $keys) {
|
||||
if (-not $defaultByKey.ContainsKey($key)) {
|
||||
[void]$differences.Add([ordered]@{ tile = $key; reason = 'missing_from_default' })
|
||||
continue
|
||||
}
|
||||
if (-not $fmaByKey.ContainsKey($key)) {
|
||||
[void]$differences.Add([ordered]@{ tile = $key; reason = 'missing_from_crt_fma3_off' })
|
||||
continue
|
||||
}
|
||||
$left = $defaultByKey[$key]
|
||||
$right = $fmaByKey[$key]
|
||||
foreach ($property in @('origin_x_voxels', 'origin_y_voxels', 'origin_z_voxels',
|
||||
'step', 'cells', 'band_chunk_lo', 'band_chunk_hi',
|
||||
'sheet_tile', 'empty', 'triangle_count', 'geometry_hash')) {
|
||||
if ([string](Get-JsonValue $left $property '') -ne [string](Get-JsonValue $right $property '')) {
|
||||
[void]$differences.Add([ordered]@{
|
||||
tile = $key
|
||||
reason = "property_mismatch:$property"
|
||||
default = Get-JsonValue $left $property $null
|
||||
crt_fma3_off = Get-JsonValue $right $property $null
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
$evidence.compared_tile_count = [Math]::Min($defaultByKey.Count, $fmaByKey.Count)
|
||||
$evidence.first_differences = @($differences | Select-Object -First 10)
|
||||
$leftConfig = Get-JsonValue $defaultJson 'runtime_config' $null
|
||||
$rightConfig = Get-JsonValue $fmaJson 'runtime_config' $null
|
||||
$evidence.runtime_config_match = (ConvertTo-CompactJson $leftConfig) -eq (ConvertTo-CompactJson $rightConfig)
|
||||
$evidence.default_runtime_config = $leftConfig
|
||||
$evidence.crt_fma3_off_runtime_config = $rightConfig
|
||||
if ($differences.Count -eq 0 -and $evidence.runtime_config_match -and
|
||||
$defaultByKey.Count -eq $fmaByKey.Count) {
|
||||
$evidence.status = 'passed'
|
||||
} else {
|
||||
$evidence.reason = if ($differences.Count -gt 0) { 'per-tile geometry differs' }
|
||||
elseif (-not $evidence.runtime_config_match) { 'runtime configuration differs' }
|
||||
else { 'tile record counts differ' }
|
||||
}
|
||||
return $evidence
|
||||
}
|
||||
|
||||
function Get-SurfaceFallMetrics([string]$LogPath) {
|
||||
$lineMatch = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeSurfaceFallTest\] result=' -ErrorAction SilentlyContinue |
|
||||
Select-Object -Last 1)
|
||||
@@ -710,13 +992,43 @@ function Get-CollisionGateStressMetrics([string]$LogPath) {
|
||||
if ($result.Success -and $result.Groups['v'].Value -eq 'PASS') { $metrics.status = 'passed' }
|
||||
foreach ($key in @('reason', 'elapsed_s', 'distance_m', 'expected_distance_m', 'speed_cm_s',
|
||||
'gate_holds', 'gate_total_s', 'max_gate_s', 'full_stops',
|
||||
'full_stop_total_s', 'collision_gate')) {
|
||||
'full_stop_total_s', 'collision_gate', 'gate_progress_m', 'support_tile',
|
||||
'support_first_check_to_submit_s',
|
||||
'support_first_check_to_collision_ready_s', 'test_support_tile',
|
||||
'test_support_first_check_to_submit_s',
|
||||
'test_support_first_check_to_collision_ready_s', 'landed', 'terrain_samples',
|
||||
'terrain_measurement_missing', 'start_height_crossed',
|
||||
'min_clearance_cm', 'min_clearance_position', 'min_feet_density',
|
||||
'min_surface_vox_z', 'final_position')) {
|
||||
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
|
||||
if ($match.Success) { $metrics[$key] = $match.Groups['v'].Value }
|
||||
}
|
||||
return $metrics
|
||||
}
|
||||
|
||||
function Get-VisualStalenessMetrics([string]$LogPath) {
|
||||
$lineMatch = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeVisualStaleness\] result=' -ErrorAction SilentlyContinue |
|
||||
Select-Object -Last 1)
|
||||
if ($lineMatch.Count -eq 0) { return [ordered]@{ status = 'missing'; log = $LogPath } }
|
||||
$line = [string]$lineMatch[0].Line
|
||||
$metrics = [ordered]@{ status = 'failed'; log = $LogPath; raw = $line }
|
||||
$result = [regex]::Match($line, 'result=(?<v>PASS|FAIL)')
|
||||
if ($result.Success -and $result.Groups['v'].Value -eq 'PASS') { $metrics.status = 'passed' }
|
||||
foreach ($key in @('mode', 'edit_applied', 'edit_resolved', 'edit_stale_duration_s',
|
||||
'edit_stale_tiles_max', 'lod_overlap_max_s', 'lod_overlap_events',
|
||||
'lod_overlap_pairs_max')) {
|
||||
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
|
||||
if (-not $match.Success) { continue }
|
||||
if ($key -in @('mode', 'edit_applied', 'edit_resolved', 'edit_stale_tiles_max',
|
||||
'lod_overlap_events', 'lod_overlap_pairs_max')) {
|
||||
$metrics[$key] = [int]$match.Groups['v'].Value
|
||||
} else {
|
||||
$metrics[$key] = [double]$match.Groups['v'].Value
|
||||
}
|
||||
}
|
||||
return $metrics
|
||||
}
|
||||
|
||||
function Get-TestMetrics($Ledger) {
|
||||
if ($null -eq $Ledger) { return @() }
|
||||
$logPaths = @($Ledger.log, $Ledger.stdout, $Ledger.stderr) | Where-Object {
|
||||
@@ -876,7 +1188,17 @@ try {
|
||||
Ensure-Directory (Join-Path $RunRoot 'Logs')
|
||||
Ensure-Directory (Join-Path $RunRoot 'User')
|
||||
Ensure-Directory (Join-Path $RunRoot 'ZenData')
|
||||
Ensure-Directory (Join-Path $RunRoot 'DDC')
|
||||
$DDCPath = Join-Path $RunRoot 'DDC'
|
||||
if (-not [string]::IsNullOrWhiteSpace($SharedDDC)) {
|
||||
if (-not [IO.Path]::IsPathRooted($SharedDDC)) {
|
||||
throw "-SharedDDC must be an absolute path: $SharedDDC"
|
||||
}
|
||||
$DDCPath = Get-FullPath $SharedDDC
|
||||
if (-not (Test-UnderPath $DDCPath (Join-Path $PluginRoot 'Saved'))) {
|
||||
throw "-SharedDDC must stay under the plugin Saved directory: $DDCPath"
|
||||
}
|
||||
}
|
||||
Ensure-Directory $DDCPath
|
||||
$localBefore = Get-StateSnapshot $LocalAppDataRoot
|
||||
|
||||
foreach ($required in @($EditorExe, $UbtDll, $HostProject, $ProjectFile)) {
|
||||
@@ -967,6 +1289,94 @@ try {
|
||||
(Join-Path $RunRoot 'Logs\probe.log') (Join-Path $RunRoot 'Logs\probe.stdout.log') (Join-Path $RunRoot 'Logs\probe.stderr.log')
|
||||
$CommandletMetrics = Get-CommandletMetrics $commandletOut 'owner probe'
|
||||
}
|
||||
'horizon' {
|
||||
$tracePath = Join-Path $RunRoot 'horizon_trace.jsonl'
|
||||
$baseCvars['voxel.TestRenderDistanceChunks'] = $HorizonRenderDistanceChunks
|
||||
$baseCvars['voxel.TestMaxClipLevel'] = $HorizonLevel
|
||||
$baseCvars['voxel.TestFarSheetRing'] = 0
|
||||
$baseCvars['voxel.StartupTraceFile'] = $tracePath
|
||||
$baseCvars['voxel.StartupTraceMaxTiles'] = 16384
|
||||
$baseCvars['voxel.TestExitOnSteady'] = 1
|
||||
$baseCvars['voxel.OuterClassifierMode'] = 0
|
||||
$args = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\horizon.log') (Merge-Cvars $baseCvars)
|
||||
$gameLedger = Invoke-UnrealLaunch "horizon-l$($HorizonLevel)-$($HorizonRenderDistanceChunks)-game" $args `
|
||||
(Join-Path $RunRoot 'Logs\horizon.log') (Join-Path $RunRoot 'Logs\horizon.stdout.log') (Join-Path $RunRoot 'Logs\horizon.stderr.log')
|
||||
$GameMetrics = Get-StreamingMetrics (Join-Path $RunRoot 'Logs\horizon.log')
|
||||
$HorizonMetrics = Get-HorizonMetrics $tracePath $gameLedger $GameMetrics
|
||||
if ($HorizonMetrics.status -ne 'ok') {
|
||||
Add-Failure "horizon measurement trace did not settle: $($HorizonMetrics.status)"
|
||||
}
|
||||
}
|
||||
'horizon-render' {
|
||||
$tracePath = Join-Path $RunRoot 'horizon_render_trace.jsonl'
|
||||
$baseCvars['voxel.TestRenderDistanceChunks'] = $HorizonRenderDistanceChunks
|
||||
$baseCvars['voxel.TestMaxClipLevel'] = $HorizonLevel
|
||||
$baseCvars['voxel.TestFarSheetRing'] = 0
|
||||
$baseCvars['voxel.StartupTraceFile'] = $tracePath
|
||||
$baseCvars['voxel.StartupTraceMaxTiles'] = 16384
|
||||
$baseCvars['voxel.TestCeilingView'] = 1
|
||||
$baseCvars['voxel.TestCeilingCaptureDelaySeconds'] = 90
|
||||
$baseCvars['voxel.TestExitSeconds'] = 180
|
||||
$baseCvars['voxel.OuterClassifierMode'] = 0
|
||||
$renderLog = Join-Path $RunRoot 'Logs\horizon-render.log'
|
||||
$args = New-CommonArguments 'game' $renderLog (Merge-Cvars $baseCvars) $true
|
||||
$gameLedger = Invoke-UnrealLaunch "horizon-l$($HorizonLevel)-$($HorizonRenderDistanceChunks)-render" $args `
|
||||
$renderLog (Join-Path $RunRoot 'Logs\horizon-render.stdout.log') (Join-Path $RunRoot 'Logs\horizon-render.stderr.log')
|
||||
$GameMetrics = Get-StreamingMetrics $renderLog
|
||||
$HorizonMetrics = Get-HorizonMetrics $tracePath $gameLedger $GameMetrics
|
||||
$HorizonRenderMetrics = Get-HorizonRenderMetrics $gameLedger $renderLog
|
||||
if ($HorizonMetrics.status -ne 'ok') {
|
||||
Add-Failure "horizon render trace did not settle: $($HorizonMetrics.status)"
|
||||
}
|
||||
if ($HorizonRenderMetrics.status -ne 'passed') {
|
||||
Add-Failure "horizon render did not produce a capture: $($HorizonRenderMetrics.status)"
|
||||
}
|
||||
}
|
||||
'determinism' {
|
||||
$defaultDump = Join-Path $RunRoot 'tile_hashes_default.json'
|
||||
$crtFma3OffDump = Join-Path $RunRoot 'tile_hashes_crt_fma3_off.json'
|
||||
$defaultTrace = Join-Path $RunRoot 'determinism_default_trace.jsonl'
|
||||
$crtFma3OffTrace = Join-Path $RunRoot 'determinism_crt_fma3_off_trace.jsonl'
|
||||
$determinismBase = [ordered]@{
|
||||
'voxel.TestRenderDistanceChunks' = $HorizonRenderDistanceChunks
|
||||
'voxel.TestMaxClipLevel' = $HorizonLevel
|
||||
'voxel.TestFarSheetRing' = 0
|
||||
'voxel.TestExitOnSteady' = 1
|
||||
'voxel.StartupTraceMaxTiles' = 16384
|
||||
'voxel.OuterClassifierMode' = 0
|
||||
}
|
||||
$defaultCvars = [ordered]@{}
|
||||
foreach ($key in $determinismBase.Keys) { $defaultCvars[$key] = $determinismBase[$key] }
|
||||
$defaultCvars['voxel.StartupTraceFile'] = $defaultTrace
|
||||
$defaultCvars['voxel.TileHashDump'] = $defaultDump
|
||||
$defaultLog = Join-Path $RunRoot 'Logs\determinism-default.log'
|
||||
$defaultArgs = New-CommonArguments 'game' $defaultLog (Merge-Cvars $defaultCvars)
|
||||
$defaultLedger = Invoke-UnrealLaunch "determinism-default-l$($HorizonLevel)-$($HorizonRenderDistanceChunks)" $defaultArgs `
|
||||
$defaultLog (Join-Path $RunRoot 'Logs\determinism-default.stdout.log') (Join-Path $RunRoot 'Logs\determinism-default.stderr.log')
|
||||
$defaultMetrics = Get-HorizonMetrics $defaultTrace $defaultLedger (Get-StreamingMetrics $defaultLog)
|
||||
|
||||
$crtFma3OffCvars = [ordered]@{}
|
||||
foreach ($key in $determinismBase.Keys) { $crtFma3OffCvars[$key] = $determinismBase[$key] }
|
||||
$crtFma3OffCvars['voxel.StartupTraceFile'] = $crtFma3OffTrace
|
||||
$crtFma3OffCvars['voxel.TileHashDump'] = $crtFma3OffDump
|
||||
$crtFma3OffCvars['voxel.CrtFma3'] = 0
|
||||
$crtFma3OffLog = Join-Path $RunRoot 'Logs\determinism-crt-fma3-off.log'
|
||||
$crtFma3OffArgs = New-CommonArguments 'game' $crtFma3OffLog (Merge-Cvars $crtFma3OffCvars)
|
||||
$crtFma3OffLedger = Invoke-UnrealLaunch "determinism-crt-fma3-off-l$($HorizonLevel)-$($HorizonRenderDistanceChunks)" $crtFma3OffArgs `
|
||||
$crtFma3OffLog (Join-Path $RunRoot 'Logs\determinism-crt-fma3-off.stdout.log') (Join-Path $RunRoot 'Logs\determinism-crt-fma3-off.stderr.log')
|
||||
$crtFma3OffMetrics = Get-HorizonMetrics $crtFma3OffTrace $crtFma3OffLedger (Get-StreamingMetrics $crtFma3OffLog)
|
||||
|
||||
$DeterminismEvidence = Compare-DeterminismDumps $defaultDump $crtFma3OffDump
|
||||
$DeterminismEvidence.level = $HorizonLevel
|
||||
$DeterminismEvidence.render_distance_chunks = $HorizonRenderDistanceChunks
|
||||
$DeterminismEvidence.far_sheet_ring = $false
|
||||
$DeterminismEvidence.default_measurement = $defaultMetrics
|
||||
$DeterminismEvidence.crt_fma3_off_measurement = $crtFma3OffMetrics
|
||||
$GameMetrics = $crtFma3OffMetrics
|
||||
if ($DeterminismEvidence.status -ne 'passed') {
|
||||
Add-Failure "determinism comparison failed: $(Get-JsonValue $DeterminismEvidence 'reason' 'unknown difference')"
|
||||
}
|
||||
}
|
||||
'perf' {
|
||||
$baseCvars['voxel.TestExitSeconds'] = 15
|
||||
$baseCvars['voxel.ProfileTileGeneration'] = 1
|
||||
@@ -978,6 +1388,40 @@ try {
|
||||
(Join-Path $RunRoot 'Logs\perf.log') (Join-Path $RunRoot 'Logs\perf.stdout.log') (Join-Path $RunRoot 'Logs\perf.stderr.log')
|
||||
$GameMetrics = Get-StreamingMetrics (Join-Path $RunRoot 'Logs\perf.log')
|
||||
}
|
||||
'visual-edit' {
|
||||
$baseCvars['voxel.TestVisualStaleness'] = 1
|
||||
$baseCvars['voxel.TestModification'] = 1
|
||||
$baseCvars['voxel.TestModificationTarget'] = 'loaded-geometry'
|
||||
$baseCvars['voxel.TestExitSeconds'] = 45
|
||||
$baseCvars['voxel.TestMaxClipLevel'] = 5
|
||||
$baseCvars['voxel.OuterClassifierMode'] = 0
|
||||
$visualLog = Join-Path $RunRoot 'Logs\visual-edit.log'
|
||||
$args = New-CommonArguments 'game' $visualLog (Merge-Cvars $baseCvars)
|
||||
$gameLedger = Invoke-UnrealLaunch 'visual-edit-game' $args `
|
||||
$visualLog (Join-Path $RunRoot 'Logs\visual-edit.stdout.log') (Join-Path $RunRoot 'Logs\visual-edit.stderr.log')
|
||||
$VisualStalenessMetrics = Get-VisualStalenessMetrics $visualLog
|
||||
if ($VisualStalenessMetrics.status -ne 'passed') {
|
||||
Add-Failure "visual edit repro did not pass: $($VisualStalenessMetrics.status)"
|
||||
}
|
||||
}
|
||||
'visual-lod' {
|
||||
$baseCvars['voxel.TestVisualStaleness'] = 2
|
||||
$baseCvars['voxel.TestMove'] = 1
|
||||
$baseCvars['voxel.TestMoveSpeedCmPerSecond'] = 6000
|
||||
$baseCvars['voxel.TestMoveStartSeconds'] = 25
|
||||
$baseCvars['voxel.TestMoveDirection'] = '1, 0, 0.15'
|
||||
$baseCvars['voxel.TestExitSeconds'] = 45
|
||||
$baseCvars['voxel.TestMaxClipLevel'] = 5
|
||||
$baseCvars['voxel.OuterClassifierMode'] = 0
|
||||
$visualLog = Join-Path $RunRoot 'Logs\visual-lod.log'
|
||||
$args = New-CommonArguments 'game' $visualLog (Merge-Cvars $baseCvars)
|
||||
$gameLedger = Invoke-UnrealLaunch 'visual-lod-game' $args `
|
||||
$visualLog (Join-Path $RunRoot 'Logs\visual-lod.stdout.log') (Join-Path $RunRoot 'Logs\visual-lod.stderr.log')
|
||||
$VisualStalenessMetrics = Get-VisualStalenessMetrics $visualLog
|
||||
if ($VisualStalenessMetrics.status -ne 'passed') {
|
||||
Add-Failure "visual LOD repro did not pass: $($VisualStalenessMetrics.status)"
|
||||
}
|
||||
}
|
||||
'surface-fall' {
|
||||
$baseCvars['voxel.TestSurfaceFall'] = 1
|
||||
$baseCvars['voxel.TestSurfaceFallSpawnHeightVoxels'] = 20
|
||||
@@ -1012,7 +1456,6 @@ try {
|
||||
'gate-stress' {
|
||||
$baseCvars['voxel.TestCollisionGateStress'] = 1
|
||||
$baseCvars['voxel.TestCollisionGateStressSpeedCmPerSecond'] = 6000
|
||||
$baseCvars['voxel.TestCollisionGateStressStartDelaySeconds'] = 0.1
|
||||
$baseCvars['voxel.TestCollisionGateStressDurationSeconds'] = 2
|
||||
$baseCvars['voxel.TestCollisionGateStressTimeoutSeconds'] = 30
|
||||
$args = New-CommonArguments 'game' (Join-Path $RunRoot 'Logs\gate-stress.log') (Merge-Cvars $baseCvars)
|
||||
@@ -1067,7 +1510,6 @@ try {
|
||||
$stressBaseCvars = [ordered]@{
|
||||
'voxel.TestCollisionGateStress' = 1
|
||||
'voxel.TestCollisionGateStressSpeedCmPerSecond' = 6000
|
||||
'voxel.TestCollisionGateStressStartDelaySeconds' = 0.1
|
||||
'voxel.TestCollisionGateStressDurationSeconds' = 2
|
||||
'voxel.TestCollisionGateStressTimeoutSeconds' = 30
|
||||
}
|
||||
@@ -1151,6 +1593,7 @@ $result = [ordered]@{
|
||||
scenario = $Scenario
|
||||
label = $Label
|
||||
run_directory = $RunRoot
|
||||
ddc_path = $DDCPath
|
||||
project = $ProjectFile
|
||||
staged_host = $HostProject
|
||||
assets_mode = $Assets
|
||||
@@ -1170,8 +1613,12 @@ $result = [ordered]@{
|
||||
probe_amplitudes = if ($CommandletMetrics.Contains('probe_amplitudes')) { $CommandletMetrics.probe_amplitudes } else { [ordered]@{} }
|
||||
commandlet = $CommandletMetrics
|
||||
game = $GameMetrics
|
||||
horizon = $HorizonMetrics
|
||||
horizon_render = $HorizonRenderMetrics
|
||||
determinism = $DeterminismEvidence
|
||||
surface_fall = $SurfaceFallMetrics
|
||||
strate_crossing = $StrateCrossingMetrics
|
||||
visual_staleness = $VisualStalenessMetrics
|
||||
collision_gate_stress = $CollisionGateStressMetrics
|
||||
worker_seconds = [ordered]@{
|
||||
commandlet = if ($CommandletMetrics.Contains('worker_seconds')) { $CommandletMetrics.worker_seconds } else { $null }
|
||||
@@ -1196,6 +1643,7 @@ try {
|
||||
[void]$summaryLines.Add("$(if ($pass) { 'PASS' } else { 'FAIL' }) $reason")
|
||||
[void]$summaryLines.Add("scenario=$Scenario label=$Label")
|
||||
[void]$summaryLines.Add("run_directory=$RunRoot")
|
||||
[void]$summaryLines.Add("ddc_path=$DDCPath")
|
||||
[void]$summaryLines.Add("assets=$Assets")
|
||||
[void]$summaryLines.Add("canonical_baseline=$(ConvertTo-CompactJson $result.canonical_baseline)")
|
||||
[void]$summaryLines.Add("capability_counts=$(ConvertTo-CompactJson $result.capability_counts)")
|
||||
@@ -1204,9 +1652,13 @@ try {
|
||||
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
|
||||
[void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)")
|
||||
[void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)")
|
||||
[void]$summaryLines.Add("visual_staleness=$(ConvertTo-CompactJson $result.visual_staleness)")
|
||||
[void]$summaryLines.Add("collision_gate_stress=$(ConvertTo-CompactJson $result.collision_gate_stress)")
|
||||
[void]$summaryLines.Add("probe_amplitudes=$(ConvertTo-CompactJson $result.probe_amplitudes)")
|
||||
[void]$summaryLines.Add("worker_seconds=$(ConvertTo-CompactJson $result.worker_seconds)")
|
||||
[void]$summaryLines.Add("horizon=$(ConvertTo-CompactJson $result.horizon)")
|
||||
[void]$summaryLines.Add("horizon_render=$(ConvertTo-CompactJson $result.horizon_render)")
|
||||
[void]$summaryLines.Add("determinism=$(ConvertTo-CompactJson $result.determinism)")
|
||||
[void]$summaryLines.Add("p50_p95=$(ConvertTo-CompactJson $result.p50_p95)")
|
||||
[void]$summaryLines.Add("tests=$(ConvertTo-CompactJson $result.tests)")
|
||||
[void]$summaryLines.Add("parity=$(ConvertTo-CompactJson $result.parity)")
|
||||
|
||||
Reference in new issue
Block a user