10 Commits
Author SHA1 Message Date
Fr0zkaandClaude Opus 5.5 b4ac9969f5 fix(passages): skip proofs cover the new descent geometry -- soundness PASS; passage tiles +28% cost
- Passage proof bounds now enclose everything the passage code can write: separated switchback
  legs, outward door approaches, native floor slabs with a derived relief supremum, and the final
  body-envelope carve (segment XY extent + half-width, floor range + player height + step).
- ClassifyTile: the cave-only AllSolid shortcut could restore a solid verdict after a passage
  carve candidate was found; it now keeps the tile mixed.
- Descent corridor test asserts no corridor stencil tile gets a uniform verdict.

Evidence: SealedSolidProofSoundness and ClassifyTileSoundness pass; 64 owner seeds, 0 body
blockers, 0 uniform stencil tiles, CRCs match. Negative control (passage reach shrunk to 4 voxels):
7 sealed-solid proofs among 620 corridor tiles, i.e. the check bites.

KNOWN COST, not fixed: tile profile, owner world, seed 0, 15 s window: passage-candidate tiles
83.6 ms (de05e29) -> 107.2 ms (+28%), all tiles +5.4%. Two segment-rejection filters did not help
and were removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-28 10:03:34 +02:00
Fr0zkaandClaude Opus 5.5 fb052716c6 wip(passages): A->B descent corridor clear -- 60/64 seeds blocked -> 0/64; soundness + cost not yet checked
What the pillars were: at switchbacks the native-floor D-section clip in
UVoxelStrateManager::EvaluateModifierSDF used ONE floor branch (the nearest by projection) for
neighbouring capsule segments, so one leg's floor clipped the other leg's carve and left rock
standing in the corridor (seed 0 witness: density -29.45, carve SDF +3.28; with
voxel.TunnelAblateNativeFloor=1 the same point is air). Separately, the lower ramp was routed
through the lower landing room (sign error in the lower "outward" score), where room ownership
correctly erased the tunnel floor: up to 12.5-voxel drops.

Fix: each segment clips with its own floor branch before the union; switchback legs separated
and door approaches moved outward so ramps leave their landing rooms; a final carve clears the
player body envelope (1.36 + 2x1 voxel wide, 1.8..8.8 voxels above the route floor) after all
writers, with a room-SDF guard. No floor slab, no fill, no blockade on the through edge.

Owner world, 64-seed sweep, stations every 0.5 voxel x 5 lateral x 14 heights:
before 89,603 body blockers in 60/64 seeds; after 0 in 64/64, 0 floor/width gaps, worst step
0.958 voxels (limit 1.8). Seed 0 built twice: identical density CRC.
Crossing at default speed still stops on capsule_overlaps_pending_floor (also before); 500 cm/s
reaches B.

NOT YET CHECKED: sealed-solid / classifier soundness against the moved passage geometry, and the
generation cost of the per-segment projection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-25 13:42:58 +02:00
Fr0zkaandClaude Opus 5.5 de05e293aa docs(codemap): aligned clipmap rings, indexed reconcile, immediate gate support, two new tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-25 06:11:41 +02:00
Fr0zkaandClaude Opus 5.5 6ee433efa5 test(gate): stress repro judges terrain penetration -- gate on 10/10 clear, gate off 3/3 falls through
The old criteria (travel distance) could not tell a working gate from a broken one: every run,
gate on or off, ended at (455.7, 0, -58.1). That spot is measured air ~1.2 m above a real floor
(density zero-crossing at world Z ~ -275.7 cm), so it is a terrain trap, not a fall-through.

The stress test now drops the pawn fast onto an unready support tile (0,0,-1) and samples the
density at the capsule feet every frame, sweeping at <= half a voxel between positions. PASS =
no measured clearance below -5 cm.

Owner config (radius 3, max level 6, distance 768, sheets off):
- gate on: 10/10 PASS, min clearance +120.5..+120.9 cm, one hold of 0.43-0.47 s each,
  support submitted in ~0.7 ms, collision ready in 0.43-0.47 s
- gate off: 3/3 FAIL terrain_penetration, clearance -5.7..-8.1 cm
Edit-ghost, clipmap no-overlap sweep: pass. Test-harness code only; no production path changed.
This validates a49725d.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-25 06:11:08 +02:00
Fr0zkaandClaude Opus 5.5 a49725d365 wip(gate): request the support tile at once; no gate timeout -- stress criteria NOT trusted yet
- RequestCollisionGateSupportTile submits the level-0 support tile immediately, even before the
  first desired set; the desired rebuild adopts the same pending key (1 early request, 1 adoption,
  1 ready callback in all 10 runs). First check -> submit went from ~2.2 s to ~1 us.
- The 2 s gate release timeout is removed: at the owner's startup the game thread applies nothing
  for ~2.2 s, so the timeout always fired and dropped the pawn. The gate holds downward Z only;
  horizontal movement stays free.
- Controls (previous round): legacy selector+reconcile failed the stress repro 0/5, aligned 1/5,
  so 85a803b did not regress the gate.

NOT VALIDATED: the stress repro's pass criteria were rewritten (support submitted/ready, a hold,
progress during a hold). 10/10 "pass" under them, but every one of those runs, and the gate-
disabled run, ends stuck at (455.7, 0, -58.1), ~1.55 m below the Z~97-100 at which the old
passing runs travelled 113 m. Whether that spot is a real pit or the pawn below the surface is
unresolved; until it is, a pass under these criteria proves nothing about falling through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-25 04:54:00 +02:00
Fr0zkaandClaude Opus 5.5 85a803b873 wip(streaming): LOD rings tile exactly once, cache race, reconcile 104x -- gate stress FAILS 3/4
- Clipmap: finer rings snap to child-pair boundaries, so no two desired tiles of different
  levels overlap. Owner config 635 overlapping pairs -> 0; LOD0 ring 343 -> 512 tiles.
  New test sweeps 6,174 cases; fails on 7486c29 as intended, passes here.
- Shared room-graph cache: Entry->Cache read/written only under the mutex (race fix).
- ReconcileReadyTransitionVisibility indexed: moving p95 32.0 ms -> 0.31 ms.
- Block-vs-scalar density equivalence test: 5.1M samples bit-identical; catches an injected
  optimistic EffectOverBox (5,612 mismatches).

UNVALIDATED for merge: collision-gate stress passed 1 of 4 runs on this code (fails at 4.56 m,
support collision unready at start); it passed on 7486c29. No control run yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-25 02:47:54 +02:00
Fr0zkaandClaude Opus 5 7486c299d4 fix(streaming): no stale geometry over its replacement -- edit ghost 40s -> 0.17s
The owner reported a wall staying visible after he carved through it, and overlapping LODs when crossing slopes at speed. Two causes: an edit only re-meshed the level-0 tiles of the touched chunks, so coarser tiles kept pre-edit geometry until something else re-queued them; and superseded tiles stayed drawn until the budgeted teardown reached them.

Now: edits fence late pre-edit async results and re-mesh every affected loaded/pending level; a replaced tile is hidden as soon as its replacement is fully covered, while destruction and collision removal stay budgeted (the anti-spike rule that made teardown budgeted in the first place). Load-before-unload is unchanged, so no holes.

Measured: edit ghost 40.095 s (unresolved at test exit) -> 0.172 s; 6000 cm/s slope overlap 19.952 s with 153 overlapping pairs -> 0.000 s with zero events. Gate stress PASS (113.68 m, 2 holds, 0 full stops). Apply-frame cost peaked at 0.979 ms against a 2 ms budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-23 21:10:20 +02:00
Fr0zkaandClaude Opus 5 f36bc1f9b2 wip(streaming): priority by class and level -- near ring 53.6s -> 1.2s, but crossing test fails
Scheduling rule: CollisionCritical -> Edited -> LOD0 -> Horizon, with coarse work capped so 4 task slots stay reserved for near work; result queues follow the same order; epoch and cancellation handling unchanged.

Measured at MaxClipLevel=5 / RenderDistanceChunks=768: spawn ring request-to-ready 53.632 s -> 1.171 s; spawn ring from trace start 68.252 -> 62.549 s; moving first complete ring 66.808 -> 63.917 s, warm ring p50 0.267 s, per-tile moving LOD0 p50 69.4 -> 62.0 ms; full settle of 10,841 tiles 110.7 -> 106.9 s. Gate stress PASS (139.99 m, 0 full stops); static perf PASS.

⚠️ NOT SAFE TO MERGE YET: the strate-crossing test fails in both patched runs with capsule_overlaps_pending_floor -- the exact guarantee the gate exists for. Codex suspects the owner's current DA_Settings (seal 50, clip level 5) rather than this change, since older passing captures used different settings. Control needed: same test, same assets, on 370c39e.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-23 19:04:13 +02:00
Fr0zkaandClaude Opus 5 370c39e473 feat(streaming): horizon distance with real tiles only -- sheets off by default
The sheet system models terrain as one ground and one cap height per column, which cannot express this world's overhangs, floating islands and open sky; three attempts to fix the MC/sheet seam produced holes, then caps over the void, then both. Measured alternative: cover the render distance with real marching-cubes tiles and switch the far-sheet ring off.

Benchmarks at RenderDistanceChunks=768, all tiles loaded at steady state: L4 57,473 tiles / 30.1M tris / 837 MiB / 2,514 s worker / 296 s to settle; L5 10,841 / 8.1M / 226 MiB / 375 s / 108 s; L6 3,281 / 2.9M / 82 MiB / 127 s / 77 s. Defaults now MaxClipLevel=5, bFarSheetRing=false. Invariants: strate crossing PASS, gate stress PASS (140.06 m, 0 full stops), determinism PASS with 10,841 tile hashes identical vs -voxel.CrtFma3=0. Peak memory ~4.2 GiB in all three.

Owner action required: his DA_Settings still serializes MaxClipLevel=3 (asset edits are out of scope for Codex), so his world needs that value raised to 5 to use the new far field. At twice the distance the estimate is ~39.5k tiles / 29-30M tris / ~0.8 GiB / ~1,500 worker-seconds, which would want L6 rather than L5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-23 17:39:49 +02:00
Fr0zkaandClaude Opus 5 ea3f0d9a52 fix(classifier): exact lattice check before a uniform verdict; ceiling cause is NOT false certificates
ParameterRoll's 5 false box verdicts are fixed: fine room-graph intervals are now checked against exact lattice samples before a uniform verdict is accepted, while the SDF bound keeps its conservative lower bound. Lattice sweep 600 tiles / 0 violations; ParameterRoll passes with 9,577 exact lattice leaves and 774 interval disagreements caught.

The owner's ceiling holes are NOT explained by this: in his recorded session outer_classifier_mode=0, solid-proof skips were 0, and air-proof skips were outside the strate. The trace points at the far-field sheet system instead: sheet tile L5 (0,1,0) is deliberately empty inside the sheet hole while its bridge tile L4 (0,1,-1) meshed 662 ceiling triangles. Continuous ceiling is NOT verified: every render launch failed (D3D12 driver error, stalls before map load, the 6 GB cap), so the round produced no matched after-render.

Cost note: this round used 3.46M tokens on Luna 6 at max, most of it burned on failed render launches. Verification by render is unreliable in this harness; verify ceiling continuity from tile/geometry data instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-09-23 06:30:05 +02:00
17 changed files with 5782 additions and 427 deletions

No files matched your search

+11 -5
View File
@@ -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)
{
+36 -6
View File
@@ -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;
+68 -12
View File
@@ -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
+15 -6
View File
@@ -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;
+146 -9
View File
@@ -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
View File
@@ -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)")