test(composer): the handmade corpus is load-bearing for QUALITY, not validity
Answers Jahni's standing wish directly: "in the best of both worlds, i'd like the system to not rely at all on the handmade ones." Three arms, same size, same measurement, same criteria: approach survival walkable mean feature-scale mean corpus blend (today) 13/16 81% 0.0661 92.3 naive uniform 7/16 44% 0.0126 64.6 constraint-sampled 7/16 44% 0.0129 31.4 ⭐ THE ANSWER IS NOT THE SURVIVAL RATE, IT IS THE QUALITY GAP. Constraint sampling produces VALID worlds -- it passes the primordial law more often than naive rolling (1 law failure vs 3) -- but the survivors are materially worse: ~5x lower walkable fraction and ~3x lower feature scale. A composer that emits technically-passing but uniformly mediocre places has not solved the problem. So the corpus carries something the declared constraints do not. §3.3 said independent rolling destroys the CORRELATIONS; stating those correlations explicitly recovers validity but not the tacit quality that authored vectors encode. That is worth knowing and it was not knowable by argument. ⇒ VERDICT: keep the corpus in production. Constraint sampling is a corpus-free BOOTSTRAP (it can seed a corpus from nothing), and PROMOTION (Tier 5) is the real route off handmade content -- dilution, not removal. Naive rolling is rejected outright. ⚠️ UNDER-POWERED, stated plainly: 16 candidates per arm, not the 256 the spec asked for -- the fixed 8M-cell box/law workload made 256x3 too slow to finish. The 5x walkable gap is large enough to be believable at n=16, but the survival rates (44% vs 44%) are not separable at that sample size. Do not quote these as precise. Authored-corpus audit, and Codex read it correctly: 3 of 12 vectors violate the proposed relations (DA_Strate3 room coverage 2.68 vs bound 0.35; Default_SurfaceWorld ground bound; Default_VerticalShafts shaft radius 5 vs roughness margin 5.875). That does NOT mean those assets are broken -- it means the new relations are conservative HYPOTHESES, not universal corpus invariants. Recorded as such. Box law held across all three arms: 768 proved, 673,486 voxels brute-forced, 0 violations. §6.2 did not reopen under corpus-free parameters. Also noted: "uniform within clamps" was underspecified in my spec -- many fields have no finite upper clamp. Both corpus-free arms use the same declared H-relative envelopes so the comparison stays honest. Build clean, CorpusFree green, WorldRadiusVoxels=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
d717bc74ff
commit
43eaf1ef0e
5 files changed
+2131
No files matched your search
+17
@@ -477,6 +477,22 @@ it. `RequiredResources` / `ProvidedResources` close the room-state hole that cha
|
||||
could not express. `VoxelForgeComposerStructureRollTest` rolls 64 candidates, measures them through
|
||||
the offline sampler, and brute-forces every uniform box verdict from the novel stack itself.
|
||||
|
||||
`EVoxelStrateCorpusFreeSamplingMode`, `VF_RollStrateParamsCorpusFree`, and
|
||||
`VF_ValidateStrateCorpusFreeConstraints` are the offline corpus-free control/experiment. They never
|
||||
read a corpus entry or strate asset: `NaiveUniform` rolls every live scalar independently inside a
|
||||
finite, code-declared envelope, while `ConstraintSampled` draws geometry quantities and derives
|
||||
dependent quantities from the generator equations before applying the envelope as a final safety
|
||||
net. `VoxelForgeComposerCorpusFreeTest.cpp` compares both arms with today's corpus blend using the
|
||||
same structure recipe, fixed measurement window, one arrival/departure law, and box-verdict brute
|
||||
force. The requested 256×3 and 64×3 runs were abandoned before aggregate output because the fixed
|
||||
8M-cell law/box workload was too slow; the completed equal-arm run is 16 candidates per arm and
|
||||
22.995 s inside the test. Its final result is **13/16 corpus-blend survivors, 7/16 naive, and
|
||||
7/16 constraint-sampled**. Survivor quality is not equivalent: corpus survivors averaged
|
||||
**0.066071 walkable / 92.307693 voxel feature scale**, versus **0.012566 / 64.571426** for naive
|
||||
and **0.012851 / 31.428572** for constrained. All three arms had **0 box-verdict violations**;
|
||||
proved boxes / checked lattice voxels were **196 / 260,876**, **310 / 412,610**, and
|
||||
**262 / 348,722** respectively.
|
||||
|
||||
Public/VoxelWorld.h + Private/VoxelWorld.cpp — ApplyComposerCandidate exposes the four
|
||||
Live Edit|Composer properties and the CallInEditor button. It preflights the candidate, pauses
|
||||
generation before installing the manager override, and calls the existing epoch-aware full reset.
|
||||
@@ -560,6 +576,7 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
|
||||
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. Bootstrap follow-up: **1,422 applications / 168 distinct fields**; **38/64 survival**, feature scale **0→376**, walkable **0.000000→0.286766** (no broadening beyond the prior run); **1,356 Mixed + 608 AllSolid + 596 AllAir = 1,204 proved boxes**, **1,602,524 lattice voxels checked, 0 violations**, 146.250 s total in the final full-namespace run. |
|
||||
| ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice, legacy and operator-stack paths; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 24 rows match. |
|
||||
| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 42 survivors, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. Focused run: **42/64 survival (65.6%)**, **64 distinct recipes**, **6,848 bootstrap applications / 107 distinct fields**, **659 proved boxes / 877,129 voxels / 0 violations**, **64 coarse filled + 64 coarse contour pairs**, **42 fine ROI filled + 42 fine contour pairs**, **184.659 s**, **0 refusals**. |
|
||||
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm run uses **16 candidates per arm** (256×3 and 64×3 were stopped before aggregate output for runtime), step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **13/16, 7/16, 7/16** survival; survivor walkable means **0.066071, 0.012566, 0.012851** and feature-scale means **92.307693, 64.571426, 31.428572**. Box checks: **196/260,876**, **310/412,610**, **262/348,722** proved/voxels, **0 violations** in every arm. |
|
||||
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, radius, type, control geometry, and bounds bit-for-bit. |
|
||||
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check every generated inter-strate passage whose destination query answers: a 16-point ring outside the mouth's carve/blend band has at least half its samples in destination air, and the endpoint matches the pure open-point result within the mouth's float envelope. This is a connectivity proxy, not a flood-fill proof. Reports checked passages and false/unanswerable archetypes; fails if it inspects zero passages. |
|
||||
| `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 fits the measurement AABB to each arrival/departure mouth pair, reports exact before/after seed-6 cell counts, reruns every negative with doubled margin, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts axis landings, 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 seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts in both legacy and operator-stack paths. |
|
||||
|
||||
@@ -470,6 +470,131 @@ and **0 violations** (1,901 Mixed, 266 AllSolid, 393 AllAir). The parameter-roll
|
||||
proved **1,204 boxes**, checked **1,602,524 voxels**, and found **0 violations** (1,356 Mixed, 608
|
||||
AllSolid, 596 AllAir). A violation would reopen §6.2; this run did not.
|
||||
|
||||
### Corpus-free measurement (2026-09-04)
|
||||
|
||||
The proposed alternative to copying correlations from examples was measured directly. The structure
|
||||
recipe is shared by all three arms, so only parameter provenance changes:
|
||||
|
||||
* **Corpus blend (today):** the existing exact-archetype weighted parent blend, ±15% spread jitter,
|
||||
and final metadata clamps.
|
||||
* **Naive uniform:** every live scalar is independently uniform in the same finite corpus-free
|
||||
envelope. No authored vector, default vector, blend, or corpus spread is read.
|
||||
* **Constraint-sampled:** independent geometry quantities are drawn; dependent quantities are
|
||||
calculated from the relations below, then the same envelope is applied as a final safety net.
|
||||
|
||||
There is an important wording issue in Part A: most fields have only `ClampMin` and no finite
|
||||
`ClampMax` (`VoxelStrateTypes.h` has 135 minimums and 52 maximums across 216 UPROPERTYs), while
|
||||
the native tunnel fields have no reflected metadata. “Uniform within clamps” is therefore undefined
|
||||
for much of the parameter space. For this measurement, `VF_GetCorpusFreeRange` declares finite
|
||||
H-relative operational envelopes for those fields; it does not use a corpus value or a default value
|
||||
as a range. The exact envelope is in `VoxelStrateComposer.cpp:1087-1278`, and both corpus-free arms
|
||||
use it so the comparison is fair. This makes the naive result conditional on an explicit finite
|
||||
support, which is a spec caveat rather than a hidden tuning target.
|
||||
|
||||
#### Declared relations
|
||||
|
||||
These are sufficient conservative relations for the experiment, derived from the existing generator
|
||||
equations. They are not claimed to be invariants of every existing authored asset; the audit below
|
||||
checks that question rather than assuming it.
|
||||
|
||||
| relation | declaration used by `ConstraintSampled` | code that supplies the geometry/math |
|
||||
|---|---|---|
|
||||
| Room placement, overlap, and isolation | Let `S=RoomSpacing`, `D=RoomDensity`, and `R=MaxRoomRadius`. Draw target area coverage `C = πR²D/S²` in `0.08..0.18`; derive `R = S*sqrt(C/(πD))`; require `C∈[0.02,0.35]` and `R≤S/2`. The coverage interval is an explicit experiment support for the room occupancy equation, not a generator hard limit. | `VoxelCaveMorphology.cpp:1305-1326` rolls room existence, jitter, and radius; `VoxelCaveMorphology.cpp:1319-1320` gives the `[0.15,0.85]` cell jitter. Implemented/validated at `VoxelStrateComposer.cpp:1522-1540` and `:3094-3104`. |
|
||||
| Tunnel reach | Neighboring-room horizontal jitter can be `1.7S` in each axis, hence `1.7S√2`; vertical center separation can be `2*R*RoomHeightRatio`. Derive `MaxTunnelLength` as at least the Euclidean sum of those bounds. This matches the generator’s distance gate. | `VoxelCaveMorphology.cpp:1388` and `:1502` reject out-of-reach links; implemented/validated at `VoxelStrateComposer.cpp:1557-1564` and `:3114-3121`. |
|
||||
| Room vertical fit | `BoundarySealThickness + max(MaxRoomRadius,OriginRoomRadius)*RoomHeightRatio < H/2`, because room centers are restricted to the seal-free interval and the room Z radius is `Radius*RoomHeightRatio`. | `VoxelCaveMorphology.cpp:137-140` and `:248-250` establish the room/seal buffer; validated at `VoxelStrateComposer.cpp:3106-3112`. |
|
||||
| Slab floor/ceiling | Worst floor is `H*FloorRelativeHeight + 1.25*FloorRoughness`; worst ceiling is `H*CeilingRelativeHeight - 1.25*CeilingRoughness`. Derive both relative heights from seal + clearance and require a two-voxel void. | `VoxelCaveMorphology.cpp:329-362` and `VoxelGenerator.cpp:2175-2212`; implemented/validated at `VoxelStrateComposer.cpp:1602-1626` and `:3143-3185`. |
|
||||
| Maze corridor | Derive `CorridorRadius = CellSize*(0.12..0.24)` so it leaves lattice wall; cap roughness below `CorridorRadius+2` so the centerline remains carveable. | `VoxelCaveMorphology.cpp:428-458` bounds the lattice; `VoxelGenerator.cpp:2412-2432` applies roughness and subtracts `2*BaseDensity`; implemented/validated at `VoxelStrateComposer.cpp:1629-1642` and `:3188-3216`. |
|
||||
| Surface ground/cap and height features | The structural height is bounded by `ElevationRange*(1+MountainStrength)+2*1.25*SurfaceRoughness`. Add conservative budgets for cliff displacement (`CliffStrength*CliffSharpness` times that spread), terrace step, layer depth, and beach width. Derive `BaseGroundRelative` above the bottom seal and `CeilingRelative` below the top seal with cap roughness, undulation, ridge, overhang height, and two-voxel headroom. Cliff is sampled as a scalar and reduced only when this derived height budget cannot contain it. | `VoxelGenerator.cpp:2470-2517` is the structural height equation; `:2537-2557` is the cliff remap; `:2607-2654` is the cap; `:2670-2686` caps overhang height. Implemented/validated at `VoxelStrateComposer.cpp:1645-1724` and `:3219-3269`. |
|
||||
| Vertical shafts and ledges | Require `ShaftMaxRadius≤ShaftSpacing/2`; require `ShaftMinRadius > 1.5*1.25*SurfaceRoughness + 0.25`; derive `ConnectorRadius > 1.5*1.25*SurfaceRoughness + 1`; derive ledge spacing greater than `2*(LedgeDepth + floor-clearance)`. | `VoxelCaveMorphology.cpp:628-654` moves landings out of roughness/ledge bands; `VoxelGenerator.cpp:3944-3954` derives tree roughness reach and `:4209-4214` applies ledges; implemented/validated at `VoxelStrateComposer.cpp:1730-1755` and `:3272-3310`. |
|
||||
| Floating islands | Require `IslandMaxRadius≤IslandSpacing/2`; derive island radius and thickness so `BoundarySealThickness + max(0.2R,ThicknessRatio*R) < H/2`; require the minimum radius to exceed roughness plus SDF blend. | `VoxelCaveMorphology.cpp:852-935` computes island radius, underside, warp, and blend pad; `VoxelGenerator.cpp:4268-4344` starts with void and fills the island; implemented/validated at `VoxelStrateComposer.cpp:1757-1780` and `:3313-3355`. |
|
||||
| Density sign/carve strength | Every solid-starting family samples `BaseDensity>0`; tunnel worm strength is derived above base density, and the maze/shaft/island paths use the existing ±`2*BaseDensity` carve/fill convention. | `VoxelGenerator.cpp:1059`, `:2085`, `:2359-2432`, `:3944-3954`, `:4200`, and `:4272`; the relation checks are in `VoxelStrateComposer.cpp:3029-3355`. |
|
||||
|
||||
Bools and enums are categorical, not arithmetic constraints. `bTunnelsFlowTowardOrigin` is a fair
|
||||
50/50 draw; `OriginRoomMaxConnections` is an integer draw in the declared `0..12` support; and
|
||||
`RoughnessNoiseType` is a uniform draw over all four enum values. Reflected boolean fields in the
|
||||
sibling families are also fair 50/50. There are currently no non-tunnel reflected enums. The
|
||||
category stream is separate from the scalar stream so neither arm’s scalar coupling changes the
|
||||
categorical policy. The sampler is deterministic in `(archetype, seed, index, mode, H)`, uses no
|
||||
retained RNG state, and clamps only after dependent equations (`VoxelStrateComposer.cpp:1782-1841`).
|
||||
|
||||
#### The actual three-way result
|
||||
|
||||
The requested **256×3** run was started and stopped after roughly six minutes without reaching an
|
||||
aggregate report. A 64×3 attempt (including a repeat after law traversal was made conditional on
|
||||
passing the fragmentation gate) also exceeded roughly 27 minutes without an aggregate report. Those
|
||||
partial runs are not data. The completed equal-arm run therefore used **16 candidates per arm**;
|
||||
all three arms ran the same 16 structure recipes, step-4 measurement over radius 256, `MaxCells=
|
||||
8,000,000`, and one fixed arrival/departure pair. Criterion precedence is **vacuous → fragmented
|
||||
(`LargestComponentShare < 0.50`) → law failed**. Law is intentionally skipped for fragmented
|
||||
candidates because the earlier criterion already decides the candidate; `law skipped` is reported
|
||||
separately. The completed test took **22.995 s** internally (**22.980 s** in roll/measurement).
|
||||
|
||||
| approach | survival | vacuous | fragmented | law failed | air range | walkable range | feature scale range |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||||
| corpus blend (today) | 13/16 (81.2%) | 0 | 1 | 2 | 0.003291..0.999878 | 0.000000..0.318692 | 4..332 |
|
||||
| naive uniform | 7/16 (43.8%) | 3 | 3 | 3 | 0.003471..1.000000 | 0.000000..0.163436 | 0..152 |
|
||||
| constraint-sampled | 7/16 (43.8%) | 3 | 5 | 1 | 0.002636..1.000000 | 0.000000..0.220178 | 0..52 |
|
||||
|
||||
The intermediate gates were corpus `16 non-vacuous / 15 largest-enough / 13 law passes`, naive
|
||||
`13 / 10 / 7`, and constrained `13 / 8 / 7`. Observed law failures were 2, 3, and 1
|
||||
respectively; the other candidates that did not reach the law were the reported fragmented cases.
|
||||
No roll, build, measurement, stack-count, channel-order, or context failure occurred.
|
||||
|
||||
The constrained survivors are valid but not as good as the corpus survivors in this run:
|
||||
|
||||
| approach | survivor count | air range (mean) | walkable range (mean) | feature scale range (mean) |
|
||||
|---|---:|---:|---:|---:|
|
||||
| corpus blend (today) | 13 | 0.022004..0.999878 (0.801809) | 0.000000..0.318692 (0.066071) | 4..332 (92.307693) |
|
||||
| naive uniform | 7 | 0.542744..0.996999 (0.856300) | 0.000068..0.044617 (0.012566) | 12..152 (64.571426) |
|
||||
| constraint-sampled | 7 | 0.801857..0.976726 (0.886178) | 0.000237..0.047382 (0.012851) | 16..52 (31.428572) |
|
||||
|
||||
The constraint arm tied naive survival at 7/16, traded three naive law failures for five
|
||||
fragmentation failures, and produced a higher mean air fraction but dramatically lower walkable and
|
||||
feature-scale ranges than the corpus survivors. With only seven survivors in each corpus-free arm,
|
||||
this is evidence of a quality gap, not a claim about a stable population distribution; it is already
|
||||
enough to reject “technically valid means equivalent.”
|
||||
|
||||
#### Box-law and authored-corpus audit
|
||||
|
||||
Every arm performed 40 box probes per candidate. Non-`Mixed` verdicts were brute-forced over the
|
||||
same unit lattice: **0 violations** in all arms.
|
||||
|
||||
| approach | Mixed | AllSolid | AllAir | proved | voxels checked | violations |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| corpus blend (today) | 444 | 69 | 127 | 196 | 260,876 | 0 |
|
||||
| naive uniform | 330 | 168 | 142 | 310 | 412,610 | 0 |
|
||||
| constraint-sampled | 378 | 90 | 172 | 262 | 348,722 | 0 |
|
||||
| **total** | **1,152** | **327** | **441** | **768** | **673,486** | **0** |
|
||||
|
||||
The constraint sampler produced **0 declared-parameter violations** and stopped-on-violation count
|
||||
0. `WorldRadiusVoxels` remained **0**. The shared structure recipe had 0 determinism failures;
|
||||
each arm had 0 parameter-roll determinism failures, 0 invalid stack counts, 0 channel-order
|
||||
failures, and 0 context violations.
|
||||
|
||||
The same declared relations were audited against all 12 current corpus vectors. **3/12 violate**
|
||||
the conservative experiment relations:
|
||||
|
||||
* `DA_Strate3` (`TunnelNetwork`, H=128): room area coverage `2.68447`, above the declared `0.35`
|
||||
upper support.
|
||||
* `Default_SurfaceWorld` (`SurfaceWorld`, H=128): conservative ground lower bound `-15.35`,
|
||||
below the bottom seal `4`.
|
||||
* `Default_VerticalShafts` (`VerticalShafts`, H=128): minimum shaft radius `5`, below the roughness
|
||||
margin `5.875`.
|
||||
|
||||
These are findings about the proposed sufficient relations, not proof that those authored worlds
|
||||
are visibly broken: two are deliberately worst-case bounds and the first is an occupancy heuristic.
|
||||
The corpus is living outside the new constraint envelope in three places, so the envelope cannot be
|
||||
silently promoted to a universal validator without either weakening it or changing those assets.
|
||||
|
||||
#### Verdict
|
||||
|
||||
The corpus is still load-bearing for quality. Keep the corpus-blend arm in production and use the
|
||||
constraint sampler as a corpus-free exploratory/bootstrap arm whose passing candidates can enter
|
||||
Tier 5 promotion. Do not replace the production arm with naive uniform rolling. The data does not
|
||||
justify dropping the handmade vectors today; it supports keeping them while promotion gradually
|
||||
dilutes their share. A future larger sample is warranted after optimizing the fixed law/box pass,
|
||||
but it should be treated as confirmation, not as permission to tune these observed counts.
|
||||
|
||||
### 3.4 The measurement pass — one grid, one flood fill, three jobs
|
||||
|
||||
Sample a candidate strate into a coarse voxel grid, flood-fill the air **once**, derive everything from
|
||||
|
||||
@@ -0,0 +1,795 @@
|
||||
// Corpus-free composer measurement: current corpus blend versus independent and constrained rolls.
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "Math/RandomStream.h"
|
||||
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// 256x3 and 64x3 were attempted first. The latter exceeded 27 minutes before reaching
|
||||
// its aggregate report because the fixed 8M-cell law/box checks are expensive for the
|
||||
// corpus-free fields. Keep the final reduced count equal across all three arms.
|
||||
constexpr int32 NumCandidates = 16;
|
||||
constexpr float LargestComponentSurvivalThreshold = 0.50f;
|
||||
constexpr int32 BoxChecksPerCandidate = 40;
|
||||
constexpr int32 BoxStep = 1;
|
||||
constexpr int32 BoxCells = 8;
|
||||
|
||||
const TCHAR* VF_ConnectivityName(EVoxelConnectivityResult Result)
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case EVoxelConnectivityResult::Connected: return TEXT("Connected");
|
||||
case EVoxelConnectivityResult::NotConnectedAtThisResolution: return TEXT("NotConnected");
|
||||
case EVoxelConnectivityResult::StartCellSolid: return TEXT("StartCellSolid");
|
||||
case EVoxelConnectivityResult::GoalCellSolid: return TEXT("GoalCellSolid");
|
||||
case EVoxelConnectivityResult::OutOfWindow: return TEXT("OutOfWindow");
|
||||
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return TEXT("CoarseLiedBudget");
|
||||
}
|
||||
return TEXT("Unknown");
|
||||
}
|
||||
|
||||
int32 VF_FloorDiv(int32 A, int32 B)
|
||||
{
|
||||
const int32 Q = A / B;
|
||||
const int32 R = A % B;
|
||||
return (R != 0 && (R < 0) != (B < 0)) ? Q - 1 : Q;
|
||||
}
|
||||
|
||||
struct FBlockSpec
|
||||
{
|
||||
EVoxelStrateParamBlock Block;
|
||||
ECaveGeneratorType Archetype;
|
||||
uint32 SeedSalt;
|
||||
};
|
||||
|
||||
static const FBlockSpec GBlockSpecs[] =
|
||||
{
|
||||
{ EVoxelStrateParamBlock::TunnelNetwork, ECaveGeneratorType::TunnelNetwork, 0x1001u },
|
||||
{ EVoxelStrateParamBlock::Slab, ECaveGeneratorType::FlatPlain, 0x1003u },
|
||||
{ EVoxelStrateParamBlock::Maze, ECaveGeneratorType::Maze, 0x1005u },
|
||||
{ EVoxelStrateParamBlock::Surface, ECaveGeneratorType::SurfaceWorld, 0x1007u },
|
||||
{ EVoxelStrateParamBlock::VerticalShaft, ECaveGeneratorType::VerticalShafts, 0x1009u },
|
||||
{ EVoxelStrateParamBlock::FloatingIsland, ECaveGeneratorType::FloatingIslands, 0x100Bu },
|
||||
};
|
||||
|
||||
void VF_CopyRollBlock(FVoxelStrateArchetypeParams& OutParams,
|
||||
const FVoxelStrateRollInfo& Roll,
|
||||
ECaveGeneratorType Archetype)
|
||||
{
|
||||
switch (Archetype)
|
||||
{
|
||||
case ECaveGeneratorType::TunnelNetwork:
|
||||
case ECaveGeneratorType::Underwater:
|
||||
OutParams.TunnelNetworkParams = Roll.ArchetypeParams.TunnelNetworkParams;
|
||||
break;
|
||||
case ECaveGeneratorType::FlatPlain:
|
||||
case ECaveGeneratorType::CrystalChamber:
|
||||
OutParams.SlabParams = Roll.ArchetypeParams.SlabParams;
|
||||
break;
|
||||
case ECaveGeneratorType::Maze:
|
||||
OutParams.MazeParams = Roll.ArchetypeParams.MazeParams;
|
||||
break;
|
||||
case ECaveGeneratorType::SurfaceWorld:
|
||||
OutParams.SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
|
||||
break;
|
||||
case ECaveGeneratorType::VerticalShafts:
|
||||
OutParams.VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
|
||||
break;
|
||||
case ECaveGeneratorType::FloatingIslands:
|
||||
OutParams.FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void VF_SetAllRuntimeBounds(FVoxelStrateArchetypeParams& Params,
|
||||
float TopWorldZ, float BottomWorldZ)
|
||||
{
|
||||
Params.TunnelNetworkParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.TunnelNetworkParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.SlabParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.SlabParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.MazeParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.MazeParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.SurfaceParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.SurfaceParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.VerticalShaftParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.VerticalShaftParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
Params.FloatingIslandParams.StrateTopWorldZ = TopWorldZ;
|
||||
Params.FloatingIslandParams.StrateBottomWorldZ = BottomWorldZ;
|
||||
}
|
||||
|
||||
class FStackDensitySampler final : public IVoxelStrateDensitySampler
|
||||
{
|
||||
public:
|
||||
explicit FStackDensitySampler(const FVoxelOpStack& InStack) : Stack(InStack) {}
|
||||
|
||||
float SampleDensity(float WorldX, float WorldY, float WorldZ) const override
|
||||
{
|
||||
return Stack.EvalMC(WorldX, WorldY, WorldZ);
|
||||
}
|
||||
|
||||
private:
|
||||
const FVoxelOpStack& Stack;
|
||||
};
|
||||
|
||||
struct FBoxVerdictReport
|
||||
{
|
||||
int32 Mixed = 0;
|
||||
int32 AllSolid = 0;
|
||||
int32 AllAir = 0;
|
||||
int32 Proved = 0;
|
||||
int64 CheckedVoxels = 0;
|
||||
int32 Violations = 0;
|
||||
FString FirstViolation;
|
||||
};
|
||||
|
||||
FBoxVerdictReport VF_CheckStackBoxVerdicts(const FVoxelOpStack& Stack,
|
||||
const FVoxelOpContext& Context,
|
||||
int32 CandidateIndex,
|
||||
int32 BottomVoxelZ,
|
||||
int32 TopVoxelZ)
|
||||
{
|
||||
FBoxVerdictReport Report;
|
||||
constexpr int32 Extent = BoxStep * BoxCells;
|
||||
constexpr int32 SpanCells = 40;
|
||||
const int32 LoTile = VF_FloorDiv(BottomVoxelZ, Extent);
|
||||
const int32 HiTile = FMath::Max(LoTile, VF_FloorDiv(TopVoxelZ, Extent));
|
||||
FRandomStream Rng(0x6C617731 ^ CandidateIndex);
|
||||
|
||||
for (int32 BoxIndex = 0; BoxIndex < BoxChecksPerCandidate; ++BoxIndex)
|
||||
{
|
||||
const FIntVector Origin(
|
||||
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||
Rng.RandRange(LoTile, HiTile) * Extent);
|
||||
const FBox Box(
|
||||
FVector((float)(Origin.X - BoxStep), (float)(Origin.Y - BoxStep),
|
||||
(float)(Origin.Z - BoxStep)),
|
||||
FVector((float)(Origin.X + (BoxCells + 1) * BoxStep),
|
||||
(float)(Origin.Y + (BoxCells + 1) * BoxStep),
|
||||
(float)(Origin.Z + (BoxCells + 1) * BoxStep)));
|
||||
const EVoxelTileClass Verdict = Stack.ClassifyBox(Box, Context);
|
||||
if (Verdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
++Report.Mixed;
|
||||
continue;
|
||||
}
|
||||
if (Verdict == EVoxelTileClass::AllSolid)
|
||||
{
|
||||
++Report.AllSolid;
|
||||
}
|
||||
else
|
||||
{
|
||||
++Report.AllAir;
|
||||
}
|
||||
++Report.Proved;
|
||||
|
||||
const int32 GridDim = BoxCells + 1;
|
||||
const bool bClaimsSolid = Verdict == EVoxelTileClass::AllSolid;
|
||||
for (int32 GZ = -1; GZ <= GridDim; ++GZ)
|
||||
for (int32 GY = -1; GY <= GridDim; ++GY)
|
||||
for (int32 GX = -1; GX <= GridDim; ++GX)
|
||||
{
|
||||
const float X = (float)(Origin.X + GX * BoxStep);
|
||||
const float Y = (float)(Origin.Y + GY * BoxStep);
|
||||
const float Z = (float)(Origin.Z + GZ * BoxStep);
|
||||
const float Density = Stack.EvalMC(X, Y, Z);
|
||||
++Report.CheckedVoxels;
|
||||
const bool bAgrees = bClaimsSolid ? (Density < 0.0f) : (Density >= 0.0f);
|
||||
if (!bAgrees)
|
||||
{
|
||||
++Report.Violations;
|
||||
if (Report.FirstViolation.IsEmpty())
|
||||
{
|
||||
Report.FirstViolation = FString::Printf(
|
||||
TEXT("candidate %d box %d origin (%d,%d,%d) said %s but MC density "
|
||||
"at (%.0f,%.0f,%.0f) was %.9g"),
|
||||
CandidateIndex, BoxIndex, Origin.X, Origin.Y, Origin.Z,
|
||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
X, Y, Z, Density);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Report;
|
||||
}
|
||||
|
||||
struct FQualityStats
|
||||
{
|
||||
int32 Count = 0;
|
||||
float AirMin = FLT_MAX;
|
||||
float AirMax = -FLT_MAX;
|
||||
float WalkableMin = FLT_MAX;
|
||||
float WalkableMax = -FLT_MAX;
|
||||
float FeatureMin = FLT_MAX;
|
||||
float FeatureMax = -FLT_MAX;
|
||||
double AirSum = 0.0;
|
||||
double WalkableSum = 0.0;
|
||||
double FeatureSum = 0.0;
|
||||
|
||||
void Add(const FVoxelStrateMetrics& Metrics)
|
||||
{
|
||||
++Count;
|
||||
AirMin = FMath::Min(AirMin, Metrics.AirFraction);
|
||||
AirMax = FMath::Max(AirMax, Metrics.AirFraction);
|
||||
WalkableMin = FMath::Min(WalkableMin, Metrics.WalkableFraction);
|
||||
WalkableMax = FMath::Max(WalkableMax, Metrics.WalkableFraction);
|
||||
FeatureMin = FMath::Min(FeatureMin, Metrics.MedianFeatureScale);
|
||||
FeatureMax = FMath::Max(FeatureMax, Metrics.MedianFeatureScale);
|
||||
AirSum += Metrics.AirFraction;
|
||||
WalkableSum += Metrics.WalkableFraction;
|
||||
FeatureSum += Metrics.MedianFeatureScale;
|
||||
}
|
||||
|
||||
FString RangeText(float Min, float Max) const
|
||||
{
|
||||
return Count > 0
|
||||
? FString::Printf(TEXT("%.6f..%.6f"), Min, Max)
|
||||
: TEXT("n/a");
|
||||
}
|
||||
|
||||
FString SummaryText() const
|
||||
{
|
||||
return Count > 0
|
||||
? FString::Printf(
|
||||
TEXT("n=%d air=%s (mean %.6f), walkable=%s (mean %.6f), feature=%s (mean %.6f)"),
|
||||
Count, *RangeText(AirMin, AirMax), (float)(AirSum / Count),
|
||||
*RangeText(WalkableMin, WalkableMax), (float)(WalkableSum / Count),
|
||||
*RangeText(FeatureMin, FeatureMax), (float)(FeatureSum / Count))
|
||||
: TEXT("n=0");
|
||||
}
|
||||
};
|
||||
|
||||
struct FArmStats
|
||||
{
|
||||
const TCHAR* Name = TEXT("unknown");
|
||||
int32 Requested = 0;
|
||||
int32 RollFailures = 0;
|
||||
int32 BuildFailures = 0;
|
||||
int32 Unmeasured = 0;
|
||||
int32 Vacuous = 0;
|
||||
int32 Fragmented = 0;
|
||||
int32 LawFailed = 0;
|
||||
int32 LawFailedObserved = 0;
|
||||
int32 LawSkipped = 0;
|
||||
int32 Survivors = 0;
|
||||
int32 NonVacuous = 0;
|
||||
int32 LargestEnough = 0;
|
||||
int32 LawPasses = 0;
|
||||
int32 DeterminismFailures = 0;
|
||||
int32 InvalidStackCount = 0;
|
||||
int32 ChannelOrderFailures = 0;
|
||||
int32 ContextViolations = 0;
|
||||
int32 LawResults[6] = { 0, 0, 0, 0, 0, 0 };
|
||||
FQualityStats AllQuality;
|
||||
FQualityStats SurvivorQuality;
|
||||
FBoxVerdictReport Box;
|
||||
FString FirstFailure;
|
||||
|
||||
void RememberFailure(const FString& Failure)
|
||||
{
|
||||
if (FirstFailure.IsEmpty())
|
||||
{
|
||||
FirstFailure = Failure;
|
||||
}
|
||||
}
|
||||
|
||||
FString SurvivalText() const
|
||||
{
|
||||
return FString::Printf(TEXT("%d/%d (%.1f%%)"), Survivors, Requested,
|
||||
Requested > 0 ? 100.0f * (float)Survivors / (float)Requested : 0.0f);
|
||||
}
|
||||
};
|
||||
|
||||
int32 VF_LawResultIndex(EVoxelConnectivityResult Result)
|
||||
{
|
||||
switch (Result)
|
||||
{
|
||||
case EVoxelConnectivityResult::Connected: return 0;
|
||||
case EVoxelConnectivityResult::NotConnectedAtThisResolution: return 1;
|
||||
case EVoxelConnectivityResult::StartCellSolid: return 2;
|
||||
case EVoxelConnectivityResult::GoalCellSolid: return 3;
|
||||
case EVoxelConnectivityResult::OutOfWindow: return 4;
|
||||
case EVoxelConnectivityResult::CoarseLiedBudgetExhausted: return 5;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
bool VF_BitEqual(float A, float B)
|
||||
{
|
||||
uint32 ABits = 0;
|
||||
uint32 BBits = 0;
|
||||
FMemory::Memcpy(&ABits, &A, sizeof(ABits));
|
||||
FMemory::Memcpy(&BBits, &B, sizeof(BBits));
|
||||
return ABits == BBits;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeComposerCorpusFreeTest,
|
||||
"VoxelForge.Composer.CorpusFree",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
(void)Parameters;
|
||||
const double TestStartSeconds = FPlatformTime::Seconds();
|
||||
|
||||
const FString SettingsPath = TEXT("/Game/VoxelForge/DA_Settings.DA_Settings");
|
||||
UVoxelSettings* AuthoredSettings = LoadObject<UVoxelSettings>(nullptr, *SettingsPath);
|
||||
if (AuthoredSettings == nullptr)
|
||||
{
|
||||
AddError(FString::Printf(TEXT("Could not load %s; corpus-free measurement did not run."),
|
||||
*SettingsPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
FVoxelStrateCorpus Corpus;
|
||||
FString CorpusReport;
|
||||
const bool bCorpusLoaded = Corpus.LoadFromAssetRegistry(CorpusReport);
|
||||
AddInfo(CorpusReport);
|
||||
TestTrue(TEXT("corpus blend arm has a usable authored corpus"),
|
||||
bCorpusLoaded && Corpus.IsValid());
|
||||
if (!bCorpusLoaded || !Corpus.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TestEqual(TEXT("corpus contains the four project vectors plus eight defaults"),
|
||||
Corpus.GetEntries().Num(), 12);
|
||||
|
||||
FTestWorld World;
|
||||
World.Build(1337, 2, true);
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
TestTrue(TEXT("corpus-free fixture keeps WorldRadiusVoxels at 0"),
|
||||
World.Settings->WorldRadiusVoxels == 0.0f);
|
||||
|
||||
constexpr int32 CandidateStrateIndex = FTestWorld::SlotFlatPlain;
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
if (!World.GetSlotVoxelZRange(CandidateStrateIndex, TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
AddError(TEXT("corpus-free fixture has no candidate Z range"));
|
||||
return false;
|
||||
}
|
||||
const float TopWorldZ = (float)TopVoxelZ + 1.0f;
|
||||
const float BottomWorldZ = (float)BottomVoxelZ;
|
||||
const float StrateHeightInVoxels = TopWorldZ - BottomWorldZ;
|
||||
|
||||
FVector ArrivalPoint = FVector::ZeroVector;
|
||||
FVector DeparturePoint = FVector::ZeroVector;
|
||||
int32 ArrivalCount = 0;
|
||||
int32 DepartureCount = 0;
|
||||
for (const FVoxelPassage& Passage : World.StrateManager->GetPassages())
|
||||
{
|
||||
if (Passage.LowerStrateIndex == CandidateStrateIndex
|
||||
&& Passage.UpperStrateIndex + 1 == CandidateStrateIndex)
|
||||
{
|
||||
ArrivalPoint = Passage.LowerPoint;
|
||||
++ArrivalCount;
|
||||
}
|
||||
if (Passage.UpperStrateIndex == CandidateStrateIndex
|
||||
&& Passage.LowerStrateIndex == CandidateStrateIndex + 1)
|
||||
{
|
||||
DeparturePoint = Passage.UpperPoint;
|
||||
++DepartureCount;
|
||||
}
|
||||
}
|
||||
TestEqual(TEXT("corpus-free candidate has exactly one arrival mouth"), ArrivalCount, 1);
|
||||
TestEqual(TEXT("corpus-free candidate has exactly one departure mouth"), DepartureCount, 1);
|
||||
|
||||
FVoxelStrateMeasureSettings MeasureSettings;
|
||||
MeasureSettings.SampleStep = 4;
|
||||
MeasureSettings.RadiusInVoxels = 256;
|
||||
MeasureSettings.CenterXY = FVector2D::ZeroVector;
|
||||
MeasureSettings.MaxCells = 8000000;
|
||||
MeasureSettings.MaxRouteRetries = 16;
|
||||
MeasureSettings.HeadroomCells = 2;
|
||||
MeasureSettings.InteriorMarginVoxels = -1;
|
||||
|
||||
FArmStats CorpusBlend;
|
||||
CorpusBlend.Name = TEXT("corpus blend (today)");
|
||||
FArmStats Naive;
|
||||
Naive.Name = TEXT("naive uniform");
|
||||
FArmStats Constraint;
|
||||
Constraint.Name = TEXT("constraint-sampled");
|
||||
|
||||
int32 RecipeDeterminismFailures = 0;
|
||||
int32 ConstraintParameterViolations = 0;
|
||||
FString FirstConstraintParameterViolation;
|
||||
int32 ConstraintBoxStopCount = 0;
|
||||
FString FirstConstraintBoxViolation;
|
||||
bool bStopAfterConstraintViolation = false;
|
||||
|
||||
const double MeasurementStartSeconds = FPlatformTime::Seconds();
|
||||
for (int32 CandidateIndex = 0;
|
||||
CandidateIndex < NumCandidates && !bStopAfterConstraintViolation;
|
||||
++CandidateIndex)
|
||||
{
|
||||
const FVoxelOpStackRecipe Recipe = VF_RollStrateStructure(
|
||||
AuthoredSettings->Seed, CandidateIndex);
|
||||
const FVoxelOpStackRecipe RepeatRecipe = VF_RollStrateStructure(
|
||||
AuthoredSettings->Seed, CandidateIndex);
|
||||
if (!VF_AreStrateStructureRecipesIdentical(Recipe, RepeatRecipe))
|
||||
{
|
||||
++RecipeDeterminismFailures;
|
||||
}
|
||||
|
||||
FArmStats* Arms[] = { &CorpusBlend, &Naive, &Constraint };
|
||||
const EVoxelStrateCorpusFreeSamplingMode Modes[] = {
|
||||
EVoxelStrateCorpusFreeSamplingMode::NaiveUniform,
|
||||
EVoxelStrateCorpusFreeSamplingMode::ConstraintSampled,
|
||||
};
|
||||
|
||||
for (int32 ArmIndex = 0; ArmIndex < 3 && !bStopAfterConstraintViolation; ++ArmIndex)
|
||||
{
|
||||
FArmStats& Arm = *Arms[ArmIndex];
|
||||
++Arm.Requested;
|
||||
FVoxelStrateArchetypeParams CandidateParams;
|
||||
bool bParameterRollsValid = true;
|
||||
bool bConstraintParameterViolationThisCandidate = false;
|
||||
|
||||
for (int32 BlockIndex = 0; BlockIndex < UE_ARRAY_COUNT(GBlockSpecs); ++BlockIndex)
|
||||
{
|
||||
const FBlockSpec& Spec = GBlockSpecs[BlockIndex];
|
||||
const int32 BlockSeed = AuthoredSettings->Seed
|
||||
^ static_cast<int32>(Spec.SeedSalt);
|
||||
FVoxelStrateRollInfo BlockRoll;
|
||||
if (ArmIndex == 0)
|
||||
{
|
||||
BlockRoll = VF_RollStrateParamsDetailedForArchetype(
|
||||
Corpus, Spec.Archetype, BlockSeed, CandidateIndex);
|
||||
const FVoxelStrateRollInfo RepeatRoll = VF_RollStrateParamsDetailedForArchetype(
|
||||
Corpus, Spec.Archetype, BlockSeed, CandidateIndex);
|
||||
if (!BlockRoll.bValid || !RepeatRoll.bValid
|
||||
|| !VF_AreStrateArchetypeParamsBitIdentical(
|
||||
BlockRoll.ArchetypeParams, RepeatRoll.ArchetypeParams, Spec.Archetype))
|
||||
{
|
||||
++Arm.DeterminismFailures;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BlockRoll = VF_RollStrateParamsCorpusFree(
|
||||
Spec.Archetype, BlockSeed, CandidateIndex, Modes[ArmIndex - 1],
|
||||
StrateHeightInVoxels);
|
||||
const FVoxelStrateRollInfo RepeatRoll = VF_RollStrateParamsCorpusFree(
|
||||
Spec.Archetype, BlockSeed, CandidateIndex, Modes[ArmIndex - 1],
|
||||
StrateHeightInVoxels);
|
||||
if (!BlockRoll.bValid || !RepeatRoll.bValid
|
||||
|| !VF_AreStrateArchetypeParamsBitIdentical(
|
||||
BlockRoll.ArchetypeParams, RepeatRoll.ArchetypeParams, Spec.Archetype))
|
||||
{
|
||||
++Arm.DeterminismFailures;
|
||||
}
|
||||
if (ArmIndex == 2 && BlockRoll.bValid)
|
||||
{
|
||||
FString Violation;
|
||||
if (!VF_ValidateStrateCorpusFreeConstraints(
|
||||
Spec.Archetype, BlockRoll.ArchetypeParams,
|
||||
StrateHeightInVoxels, Violation))
|
||||
{
|
||||
++ConstraintParameterViolations;
|
||||
if (FirstConstraintParameterViolation.IsEmpty())
|
||||
{
|
||||
FirstConstraintParameterViolation = FString::Printf(
|
||||
TEXT("candidate %d block %d (%s): %s"), CandidateIndex,
|
||||
static_cast<int32>(Spec.Block),
|
||||
VF_GetStrateArchetypeName(Spec.Archetype), *Violation);
|
||||
}
|
||||
bParameterRollsValid = false;
|
||||
bConstraintParameterViolationThisCandidate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!BlockRoll.bValid)
|
||||
{
|
||||
bParameterRollsValid = false;
|
||||
++Arm.RollFailures;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d block %d roll failed: %s"), CandidateIndex,
|
||||
static_cast<int32>(Spec.Block), *BlockRoll.FailureReason));
|
||||
continue;
|
||||
}
|
||||
VF_CopyRollBlock(CandidateParams, BlockRoll, Spec.Archetype);
|
||||
}
|
||||
|
||||
if (!bParameterRollsValid)
|
||||
{
|
||||
++Arm.Unmeasured;
|
||||
if (ArmIndex == 2 && bConstraintParameterViolationThisCandidate)
|
||||
{
|
||||
// The constraint relation itself is a precondition of this arm; do not
|
||||
// spend the remaining budget hiding a violated declared relation.
|
||||
bStopAfterConstraintViolation = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
VF_SetAllRuntimeBounds(CandidateParams, TopWorldZ, BottomWorldZ);
|
||||
|
||||
FVoxelOpStack Stack;
|
||||
FVoxelOpContext Context;
|
||||
FString BuildError;
|
||||
const bool bBuilt = VF_BuildStackFromRecipe(
|
||||
Recipe, CandidateParams, AuthoredSettings->Seed, 14.0f,
|
||||
World.StrateManager.Get(), Stack, Context, &BuildError);
|
||||
if (!bBuilt)
|
||||
{
|
||||
++Arm.BuildFailures;
|
||||
++Arm.Unmeasured;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d build failed: %s"), CandidateIndex, *BuildError));
|
||||
continue;
|
||||
}
|
||||
if (Stack.Num() != Recipe.Modifiers.Num() + 7)
|
||||
{
|
||||
++Arm.InvalidStackCount;
|
||||
}
|
||||
FString ChannelError;
|
||||
if (!Stack.ValidateChannelOrder(&ChannelError))
|
||||
{
|
||||
++Arm.ChannelOrderFailures;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d channel order failed: %s"), CandidateIndex, *ChannelError));
|
||||
}
|
||||
if (Context.WorldRadiusVoxels != 0.0f)
|
||||
{
|
||||
++Arm.ContextViolations;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d changed WorldRadiusVoxels to %.9g"),
|
||||
CandidateIndex, Context.WorldRadiusVoxels));
|
||||
}
|
||||
|
||||
Stack.PrepareChunk(Context);
|
||||
FStackDensitySampler Sampler(Stack);
|
||||
const FVoxelStrateMetrics Metrics = VF_MeasureStrateWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, MeasureSettings, nullptr);
|
||||
if (!Metrics.bValid)
|
||||
{
|
||||
++Arm.Unmeasured;
|
||||
Arm.RememberFailure(FString::Printf(
|
||||
TEXT("candidate %d measurement refused: %s"), CandidateIndex,
|
||||
*Metrics.RefusalReason));
|
||||
continue;
|
||||
}
|
||||
Arm.AllQuality.Add(Metrics);
|
||||
|
||||
const bool bNonVacuous = Metrics.NumSampled > 0
|
||||
&& Metrics.NumAir > 0 && Metrics.NumSolid > 0;
|
||||
const bool bLargestEnough = bNonVacuous
|
||||
&& Metrics.LargestComponentShare >= LargestComponentSurvivalThreshold;
|
||||
|
||||
bool bLawPass = false;
|
||||
if (bLargestEnough)
|
||||
{
|
||||
FVoxelConnectivityDiagnostics Law;
|
||||
if (ArrivalCount == 1 && DepartureCount == 1)
|
||||
{
|
||||
Law = VF_DiagnoseConnectivityWithSampler(
|
||||
Sampler, BottomVoxelZ, TopVoxelZ + 1,
|
||||
Context.EdgeSealThickness, ArrivalPoint, DeparturePoint, MeasureSettings);
|
||||
++Arm.LawResults[VF_LawResultIndex(Law.Result)];
|
||||
}
|
||||
else
|
||||
{
|
||||
Law.bValid = false;
|
||||
Law.Result = EVoxelConnectivityResult::OutOfWindow;
|
||||
++Arm.LawResults[VF_LawResultIndex(Law.Result)];
|
||||
}
|
||||
|
||||
bLawPass = ArrivalCount == 1 && DepartureCount == 1
|
||||
&& Law.bValid && Law.Result == EVoxelConnectivityResult::Connected
|
||||
&& !Law.bStartSnapped && !Law.bGoalSnapped;
|
||||
if (bLawPass) { ++Arm.LawPasses; }
|
||||
if (!bLawPass) { ++Arm.LawFailedObserved; }
|
||||
}
|
||||
else
|
||||
{
|
||||
++Arm.LawSkipped;
|
||||
}
|
||||
|
||||
if (bNonVacuous) { ++Arm.NonVacuous; }
|
||||
if (bLargestEnough) { ++Arm.LargestEnough; }
|
||||
|
||||
const bool bSurvivor = bNonVacuous && bLargestEnough && bLawPass;
|
||||
if (bSurvivor)
|
||||
{
|
||||
++Arm.Survivors;
|
||||
Arm.SurvivorQuality.Add(Metrics);
|
||||
}
|
||||
else if (!bNonVacuous)
|
||||
{
|
||||
++Arm.Vacuous;
|
||||
}
|
||||
else if (!bLargestEnough)
|
||||
{
|
||||
++Arm.Fragmented;
|
||||
}
|
||||
else
|
||||
{
|
||||
++Arm.LawFailed;
|
||||
}
|
||||
|
||||
const FBoxVerdictReport BoxReport = VF_CheckStackBoxVerdicts(
|
||||
Stack, Context, CandidateIndex, BottomVoxelZ, TopVoxelZ);
|
||||
Arm.Box.Mixed += BoxReport.Mixed;
|
||||
Arm.Box.AllSolid += BoxReport.AllSolid;
|
||||
Arm.Box.AllAir += BoxReport.AllAir;
|
||||
Arm.Box.Proved += BoxReport.Proved;
|
||||
Arm.Box.CheckedVoxels += BoxReport.CheckedVoxels;
|
||||
Arm.Box.Violations += BoxReport.Violations;
|
||||
if (Arm.Box.FirstViolation.IsEmpty() && !BoxReport.FirstViolation.IsEmpty())
|
||||
{
|
||||
Arm.Box.FirstViolation = BoxReport.FirstViolation;
|
||||
}
|
||||
if (ArmIndex == 2 && BoxReport.Violations > 0)
|
||||
{
|
||||
++ConstraintBoxStopCount;
|
||||
bStopAfterConstraintViolation = true;
|
||||
FirstConstraintBoxViolation = BoxReport.FirstViolation;
|
||||
}
|
||||
}
|
||||
}
|
||||
const double MeasurementSeconds = FPlatformTime::Seconds() - MeasurementStartSeconds;
|
||||
|
||||
// Audit the same declared relations against the 12 current authored corpus vectors. Project
|
||||
// assets carry their own height; the eight C++ defaults use the fixture's 4-chunk height.
|
||||
int32 AuthoredConstraintViolations = 0;
|
||||
FString AuthoredViolationList;
|
||||
for (const FVoxelStrateCorpusEntry& Entry : Corpus.GetEntries())
|
||||
{
|
||||
float EntryHeight = 4.0f * (float)CHUNK_SIZE;
|
||||
if (!Entry.SourcePath.Contains(TEXT("/VoxelForge/ComposerDefaults/")))
|
||||
{
|
||||
UVoxelStrateDefinition* Definition = LoadObject<UVoxelStrateDefinition>(
|
||||
nullptr, *Entry.SourcePath);
|
||||
if (Definition != nullptr)
|
||||
{
|
||||
EntryHeight = FMath::Max(1.0f, (float)Definition->StrateHeightInChunks)
|
||||
* (float)CHUNK_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
FString Violation;
|
||||
if (!VF_ValidateStrateCorpusFreeConstraints(
|
||||
Entry.Archetype, Entry.ArchetypeParams, EntryHeight, Violation))
|
||||
{
|
||||
++AuthoredConstraintViolations;
|
||||
if (!AuthoredViolationList.IsEmpty())
|
||||
{
|
||||
AuthoredViolationList += TEXT("; ");
|
||||
}
|
||||
AuthoredViolationList += FString::Printf(
|
||||
TEXT("%s (%s, H=%.0f): %s"), *Entry.SourceName,
|
||||
VF_GetStrateArchetypeName(Entry.Archetype), EntryHeight, *Violation);
|
||||
}
|
||||
}
|
||||
|
||||
const FArmStats* Arms[] = { &CorpusBlend, &Naive, &Constraint };
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Corpus-free measurement: requested %d candidates per arm; structure recipes were "
|
||||
"shared across all three arms; sample step=%d radius=%d MaxCells=%d, fixed law mouths, "
|
||||
"criterion precedence is vacuous > fragmented > law failed. Runtime %.3fs, roll/measure "
|
||||
"%.3fs."),
|
||||
NumCandidates, MeasureSettings.SampleStep, MeasureSettings.RadiusInVoxels,
|
||||
MeasureSettings.MaxCells, FPlatformTime::Seconds() - TestStartSeconds, MeasurementSeconds));
|
||||
|
||||
FString Table = TEXT(
|
||||
"approach | survival | vacuous | fragmented | law failed | air range | walkable range | feature scale range\n");
|
||||
FString SurvivorTable = TEXT(
|
||||
"survivor quality (ranges are over survivors only; main table ranges are over all measured candidates)\n"
|
||||
"approach | survivors | air range (mean) | walkable range (mean) | feature scale range (mean)\n");
|
||||
for (const FArmStats* Arm : Arms)
|
||||
{
|
||||
Table += FString::Printf(
|
||||
TEXT("%s | %s | %d | %d | %d | %s | %s | %s\n"),
|
||||
Arm->Name, *Arm->SurvivalText(), Arm->Vacuous, Arm->Fragmented, Arm->LawFailed,
|
||||
*Arm->AllQuality.RangeText(Arm->AllQuality.AirMin, Arm->AllQuality.AirMax),
|
||||
*Arm->AllQuality.RangeText(Arm->AllQuality.WalkableMin, Arm->AllQuality.WalkableMax),
|
||||
*Arm->AllQuality.RangeText(Arm->AllQuality.FeatureMin, Arm->AllQuality.FeatureMax));
|
||||
SurvivorTable += FString::Printf(
|
||||
TEXT("%s | %d | %s | %s | %s\n"),
|
||||
Arm->Name, Arm->SurvivorQuality.Count,
|
||||
*Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.AirMin, Arm->SurvivorQuality.AirMax),
|
||||
*Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.WalkableMin, Arm->SurvivorQuality.WalkableMax),
|
||||
*Arm->SurvivorQuality.RangeText(Arm->SurvivorQuality.FeatureMin, Arm->SurvivorQuality.FeatureMax));
|
||||
}
|
||||
AddInfo(Table);
|
||||
AddInfo(SurvivorTable);
|
||||
|
||||
for (const FArmStats* Arm : Arms)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s: non-vacuous=%d, largest>=%.2f=%d, law pass=%d, law failures observed=%d, "
|
||||
"law skipped=%d, "
|
||||
"roll failures=%d, build failures=%d, unmeasured=%d, deterministic roll failures=%d; "
|
||||
"stack-count failures=%d, channel-order failures=%d, context violations=%d; "
|
||||
"law results Connected=%d NotConnected=%d StartSolid=%d GoalSolid=%d OutOfWindow=%d Budget=%d; "
|
||||
"quality all %s; survivor quality %s; first failure=%s."),
|
||||
Arm->Name, Arm->NonVacuous, LargestComponentSurvivalThreshold, Arm->LargestEnough,
|
||||
Arm->LawPasses, Arm->LawFailedObserved, Arm->LawSkipped,
|
||||
Arm->RollFailures, Arm->BuildFailures,
|
||||
Arm->Unmeasured, Arm->DeterminismFailures,
|
||||
Arm->InvalidStackCount, Arm->ChannelOrderFailures, Arm->ContextViolations,
|
||||
Arm->LawResults[0], Arm->LawResults[1], Arm->LawResults[2],
|
||||
Arm->LawResults[3], Arm->LawResults[4], Arm->LawResults[5],
|
||||
*Arm->AllQuality.SummaryText(), *Arm->SurvivorQuality.SummaryText(),
|
||||
Arm->FirstFailure.IsEmpty() ? TEXT("none") : *Arm->FirstFailure));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("%s box verdicts: mixed=%d AllSolid=%d AllAir=%d proved=%d voxels checked=%lld "
|
||||
"violations=%d; %s"),
|
||||
Arm->Name, Arm->Box.Mixed, Arm->Box.AllSolid, Arm->Box.AllAir, Arm->Box.Proved,
|
||||
Arm->Box.CheckedVoxels, Arm->Box.Violations,
|
||||
Arm->Box.FirstViolation.IsEmpty() ? TEXT("no violations") : *Arm->Box.FirstViolation));
|
||||
}
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Authored-corpus constraint audit: %d/%d vectors violated at their declared strate "
|
||||
"height. %s"),
|
||||
AuthoredConstraintViolations, Corpus.GetEntries().Num(),
|
||||
AuthoredViolationList.IsEmpty() ? TEXT("none") : *AuthoredViolationList));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Shared structure determinism failures=%d; per-arm channel/context violations are "
|
||||
"reported below; constraint parameter violations=%d; constraint box-stop count=%d. %s%s"),
|
||||
RecipeDeterminismFailures,
|
||||
ConstraintParameterViolations, ConstraintBoxStopCount,
|
||||
FirstConstraintParameterViolation.IsEmpty() ? TEXT("") : *FirstConstraintParameterViolation,
|
||||
FirstConstraintBoxViolation.IsEmpty() ? TEXT("")
|
||||
: *FString::Printf(TEXT("; first box violation: %s"), *FirstConstraintBoxViolation)));
|
||||
|
||||
TestEqual(TEXT("configured candidate count requested for corpus blend"), CorpusBlend.Requested, NumCandidates);
|
||||
TestEqual(TEXT("configured candidate count requested for naive uniform"), Naive.Requested, NumCandidates);
|
||||
TestEqual(TEXT("configured candidate count requested for constraint-sampled"), Constraint.Requested, NumCandidates);
|
||||
TestEqual(TEXT("corpus blend roll failures"), CorpusBlend.RollFailures, 0);
|
||||
TestEqual(TEXT("naive uniform roll failures"), Naive.RollFailures, 0);
|
||||
TestEqual(TEXT("constraint-sampled roll failures"), Constraint.RollFailures, 0);
|
||||
TestEqual(TEXT("corpus blend build failures"), CorpusBlend.BuildFailures, 0);
|
||||
TestEqual(TEXT("naive uniform build failures"), Naive.BuildFailures, 0);
|
||||
TestEqual(TEXT("constraint-sampled build failures"), Constraint.BuildFailures, 0);
|
||||
TestEqual(TEXT("corpus-free parameter determinism failures"),
|
||||
Naive.DeterminismFailures + Constraint.DeterminismFailures, 0);
|
||||
TestEqual(TEXT("corpus parameter determinism failures"), CorpusBlend.DeterminismFailures, 0);
|
||||
TestEqual(TEXT("shared structure recipe determinism failures"), RecipeDeterminismFailures, 0);
|
||||
TestEqual(TEXT("constraint-sampled declared-parameter violations"),
|
||||
ConstraintParameterViolations, 0);
|
||||
TestEqual(TEXT("constraint-sampled box verdict violations"),
|
||||
Constraint.Box.Violations, 0);
|
||||
TestEqual(TEXT("corpus blend box verdict violations"), CorpusBlend.Box.Violations, 0);
|
||||
TestEqual(TEXT("naive uniform box verdict violations"), Naive.Box.Violations, 0);
|
||||
TestEqual(TEXT("corpus blend WorldRadiusVoxels/context violations"),
|
||||
CorpusBlend.InvalidStackCount + CorpusBlend.ChannelOrderFailures
|
||||
+ CorpusBlend.ContextViolations, 0);
|
||||
TestEqual(TEXT("naive uniform WorldRadiusVoxels/context violations"),
|
||||
Naive.InvalidStackCount + Naive.ChannelOrderFailures + Naive.ContextViolations, 0);
|
||||
TestEqual(TEXT("constraint-sampled WorldRadiusVoxels/context violations"),
|
||||
Constraint.InvalidStackCount + Constraint.ChannelOrderFailures
|
||||
+ Constraint.ContextViolations, 0);
|
||||
TestTrue(TEXT("constraint sampler did not stop early on a declared relation or box violation"),
|
||||
!bStopAfterConstraintViolation);
|
||||
|
||||
const double TestSeconds = FPlatformTime::Seconds() - TestStartSeconds;
|
||||
AddInfo(FString::Printf(TEXT("CorpusFree total runtime %.3fs; all three arms requested %d each; "
|
||||
"WorldRadiusVoxels=0."), TestSeconds, NumCandidates));
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
File diff suppressed because it is too large.
Load diff
@@ -320,6 +320,35 @@ VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
|
||||
/** Pure structure roll: no retained RNG state and no corpus/runtime dependency. */
|
||||
VOXELFORGE_API FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index);
|
||||
|
||||
/**
|
||||
* Provenance for a parameter block that does not read the authored corpus.
|
||||
*
|
||||
* NaiveUniform is the §3.3 control: every scalar is rolled independently inside the same
|
||||
* finite code-declared envelope used by the constraint arm. ConstraintSampled draws the
|
||||
* independent geometry quantities and derives the coupled quantities from generator equations.
|
||||
* Neither mode reads a strate asset or a corpus entry.
|
||||
*/
|
||||
enum class EVoxelStrateCorpusFreeSamplingMode : uint8
|
||||
{
|
||||
NaiveUniform,
|
||||
ConstraintSampled,
|
||||
};
|
||||
|
||||
/** Roll one native parameter family without consulting FVoxelStrateCorpus. */
|
||||
VOXELFORGE_API FVoxelStrateRollInfo VF_RollStrateParamsCorpusFree(
|
||||
ECaveGeneratorType Archetype,
|
||||
int32 Seed,
|
||||
int32 Index,
|
||||
EVoxelStrateCorpusFreeSamplingMode Mode,
|
||||
float StrateHeightInVoxels);
|
||||
|
||||
/** Validate the hard geometry relations used by ConstraintSampled and the corpus audit. */
|
||||
VOXELFORGE_API bool VF_ValidateStrateCorpusFreeConstraints(
|
||||
ECaveGeneratorType Archetype,
|
||||
const FVoxelStrateArchetypeParams& Params,
|
||||
float StrateHeightInVoxels,
|
||||
FString& OutViolation);
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Roll one candidate for the editor walk-through path. Parameter rolls call
|
||||
|
||||
Reference in new issue
Block a user