refactor: resolved metre layout is the runtime authority (L41, migration step 3); stamp covers plugin sources; R8

UVoxelStrateManager::Initialize resolves the layout width to integer micrometres before passages or
scheduling; each slot stores physical [bottom, top) bounds, transition mode/width and passage settings;
chunk indices remain a floor-based compatibility projection. The Core adapter derives interiors and L29
bands from those resolved values. verify: full-tier cost baseline is HEAD (or --baseline), tier timeout
7200 s with 1800 s per process; the source stamp now includes the plugin Source tree compiled on Linux
(it previously missed VoxelStrateManager.cpp and friends). Owner hashes identical to L37; fast, medium
and full PASS on a clean rebuild. Unreal manager/adapter edits are uncompiled on Windows.
Research R8: inventory of unit-bearing UPROPERTYs and a custom-version asset migration plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-23 19:36:51 +02:00
1 parent efa9e89468
commit 1abee91721
11 files changed
+662 -215

No files matched your search

+31
View File
@@ -2184,6 +2184,37 @@ No Unreal adapter or `VoxelWorld.cpp` change was needed, and the evaluator versi
L37/L39 owner hashes are the parity baseline; the L40 report records fast, medium, full and paired
cost evidence.
### 8.51 Resolved metre layout authority — World Assembler M3 L41
`UVoxelStrateManager::Initialize` now resolves the layout in integer micrometres before passages or
Core work are scheduled. The configured layout width is rounded once to micrometres; legacy height
and inter-strate gap counts are multiplied by that width. Each slot stores its half-open physical
interval `[bottom, top)`, height, transition mode/width, copied passage settings and their physical
dimensions. The effective world seed is captured in the manager before passage generation.
The historical 8 m layout keeps its old placement: the first upper edge is +8 m, and each authored
chunk of height or gap is exactly 8 m. `TopChunkZ` and `BottomChunkZ` are floor-based projections of
the resolved interval, including mathematical floor for negative Z. Season schema v2 still validates
its aligned top/bottom chunk values and stacked order as before, then maps those accepted projections
to the same physical intervals. L29's facing ceiling/bedrock bands are derived from these physical
intervals and the copied transition width, so their metre bounds retain the prior chunk-based values.
The Core adapter builds SurfaceWorld and FloatingIslands bounds, shared-layout bands, and resolved
passage dimensions from the micrometre fields. FI and SW both receive the same
`LayoutChunkWidthSubvoxels = round(layoutWidthMicrometres * 256 / coreVoxelMicrometres)`; layout
conversion does not use the owner/Core scale. Owner/Core ratios remain in the legacy FloatingIslands
parameter adapter for generator dimensions that are still authored in owner voxels. The manager's
legacy passage builder converts resolved passage micrometres back to owner voxels only at its legacy
geometry API boundary.
Chunk coordinates remain in `GetStrateForChunk`, `GetGeneratorTypeForChunk`, chunk-parameter queries,
season-v2 validation, and legacy generator dispatch because those interfaces accept layout-chunk
indices. Their values are cached projections from the physical slot bounds. World-Z queries and the
Core snapshot use the half-open metre intervals. L40's bound Core field table remains the native
per-sample dispatch path; this migration adds no manager lookup to it. The Unreal manager and adapter
cannot be compiled in this Linux workspace, so their small source changes still need the Windows
Unreal build before integration.
## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+20 -15
View File
@@ -30,7 +30,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
| Term | Meaning |
|------|---------|
| Legacy chunk | 32×32×32 voxels (`CHUNK_SIZE`). At the legacy 12.5 cm voxel size it spans 4 m; it is not the strate-layout unit or the Core tile. |
| Strate-layout chunk | World-level `StrateLayoutChunkSizeMeters`, default 8 m, shared by all generators and independent of voxel size. The director resolves it once and Core reads the resolved snapshot. At 25 cm it is 32 Core voxels; at 20 cm it is 40 Core voxels. |
| Strate-layout chunk | World-level `StrateLayoutChunkSizeMeters`, default 8 m, shared by all generators and independent of voxel size. `UVoxelStrateManager::Initialize` resolves it to integer micrometres; slot bounds, heights, gap and transition widths use those physical values. Core derives the common `LayoutChunkWidthSubvoxels` from that same width. At 25 cm it is 32 Core voxels; at 20 cm it is 40 Core voxels. |
| Core tile | Internal 32×32×32 Core voxel tile. Its metre size follows the Core voxel size and stays separate from the configurable layout chunk. |
| Density | Scalar field. `< 0` solid, `> 0` air, `0` = surface (`IsoLevel`). |
| Strate | Vertical layer of the world, stacked downward. Has its own generator + params. |
@@ -70,8 +70,10 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
(RealtimeMeshComponent)
```
`UVoxelStrateManager` is the side oracle: "what strate is at this Z, what params,
what generator type, and is there a passage/elevator SDF near here?"
`UVoxelStrateManager` owns resolved half-open metre intervals (`[bottom, top)`) and is the side
oracle: "what strate is at this Z, what params, what generator type, and is there a passage/elevator
SDF near here?" Integer chunk bounds on each slot are compatibility projections for existing chunk
dispatch APIs.
---
@@ -174,7 +176,7 @@ remains `0`, and lateral regions remain gated off.
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; schema-v1 owner-asset export check and BuildHost Core actor tests. `-coreworldassembly=PATH` supplies a reviewed manifest transiently to `-coreworldtest=1`; it does not save a data asset. `FStandaloneCoreAdapterScheduler` runs Core work at 1/8/16 workers; `VerifyStandaloneCoreAdapter` checks hashes and walkability. Existing bounded render/`VF_` walk/OBJ orchestration and research options remain editor/commandlet-only and are not linked by Game/Shipping. |
| `Source/VoxelForge/Public/VoxelSettings.h` / `Private/VoxelSettings.cpp` | `UVoxelSettings` owns optional canonical world-assembly JSON and its Core digest. The editor file picker plus `ImportWorldAssemblyManifestJson` imports the reviewed manifest into the cooked settings asset and validates the recipe seed against the effective settings seed. |
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Validates the optional manifest against each resolved `SurfaceWorld` slot (unique strate index and exact top/bottom chunks), then exports canonical `resolved_layout` separately from `world_assembly` and its hash. The layout digest does not alter recipe hashes. Invalid plans fail Core initialization. |
| `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` | Resolves Core's common `LayoutChunkWidthSubvoxels` from the manager's physical layout width; builds SurfaceWorld/FloatingIslands bounds, L29 bands and passage settings from resolved micrometre slot fields; keeps owner/Core ratios in the legacy FloatingIslands parameter adapter. Chunk fields validate imported plans as compatibility projections. Exports canonical `resolved_layout` separately from `world_assembly` and its hash. Invalid plans fail Core initialization. |
| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c through step 2f. The original path still compares live `GetDensityAt` with the editor chunk program. Research modes assign contiguous batches dynamically so surface-heavy work balances across workers; steps 2e and 2f use 4³ proof bricks and stage each evaluated brick before copying contiguous X rows, while step 2d retains 8³. Each skip requires a macro Lipschitz bound plus the complete final-detail supremum; uncertain bricks retain every 12.5 cm detail sample. Timings split classification, point evaluation, and uniform fill; step 2f compares all profiles and hashes workers 1 and 6. |
| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Two default-off `WITH_EDITOR` research fields, selected by `voxel.ResearchRedefinedDensityField` (step 2d) and `voxel.ResearchParameterizedCave` (steps 2e/2f). Step 2f retains a metre-scaled 2e baseline for its rejection audit. New profiles use strongly jittered, individually rotated elliptical rooms capped against nearest neighbors and curved cubic-Bezier passages with deterministic centerline and width variation along each edge. Room heights and feature rolls vary by cell; ledges, occasional pillars, stalactites/stalagmites, basin floors, and bounded hanging masses remain features. Point and brick detail sampling share independently rotated 96/48/24-sample bands and a smooth ridge response, preserving 12/6/3 m wavelengths at 12.5 cm per sample. The parameterized mesher evaluates the exact field through prepared 4³ bricks and the same sound skip proof in commandlet and editor-game meshing. In-editor and in-game profile changes invalidate and rebuild all loaded tiles. In an editor game with this field enabled, sealed-solid, out-of-layout, and outer-classifier verdicts are declined without changing their console switches. |
| `Private/Tests/VoxelForgeParameterizedGenerationTest.cpp` | Separate three-seed functional sweep for the step 2f parameterized field; passage landing air/floor brackets and arrival-to-exit walking routes are checked without changing the live-world tests. |
@@ -598,13 +600,16 @@ content lists, audio, `GameplayTags`(393). EditConditions show/hide param groups
type.
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
Maps depth→strate at runtime; owns passages.
Resolves each slot's physical metre interval, layout gap, transition width, world seed and passage
settings during `Initialize`, then owns passages. World-Z APIs query the physical intervals; legacy
chunk APIs use the cached integer projection.
- `FVoxelPassage` (h:46): standing endpoints, radius, type, wandering control points, and
upper/lower local landing descriptors (room, floor, and door; no root connector).
- `FStrateSlot` (h:128): definition + chunk-Z range + index.
- `FStrateSlot` (h:128): content definition, resolved micrometre bounds/height, resolved transition
and passage inputs, and compatibility chunk-Z projection + strate index.
| Method | .cpp line | Role |
|--------|-----------|------|
| `Initialize` | 1164 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
| `Initialize` | 1186 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
| `GeneratePassages` | 1659 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
| `EvaluateModifierSDF` | 2643 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. |
| `ApplyPassageModifier` | 2772 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
@@ -612,18 +617,18 @@ Maps depth→strate at runtime; owns passages.
| `ApplyPassageLandingFloorMC` | 2983 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
| `AnyPassageLandingFloorNearBox` | — | Conservative local-landing-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
| `FindSlotIndexForChunkZ` | 6111 | Z → layout index. |
| `GetStrateAt` / `GetStrateIndex` | 6127 / 6139 | World-Z queries. |
| `FindSlotIndexForChunkZ` / `FindSlotIndexForWorldZCentimetres` | 6239 / 6251 | Chunk callers use the cached compatibility projection; world-Z callers search physical half-open micrometre intervals. |
| `GetStrateAt` / `GetStrateIndex` | 6276 / 6287 | World-Z queries against half-open micrometre intervals. |
| `GetLayoutVersion` | h:250 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
| `GetStrateForChunk` | 6150 | Chunk → definition. |
| `GetGeneratorTypeForChunk` | 6176 | Chunk → generator type. |
| `UsesOperatorStackForChunk` | 6203 | Chunk → does its slot have an operator stack? True for any in-layout slot with a definition whose archetype is one of the 8 (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater); false outside the layout. **That list is written down here and nowhere else.** `ClassifyTile` guards on it; `GetDensityAt` uses it to share TunnelNetwork stacks by chunk Z. |
| `GetStrateForChunk` | 6298 | Chunk → definition via the compatibility projection. |
| `GetGeneratorTypeForChunk` | 6324 | Chunk → generator type via the compatibility projection. |
| `UsesOperatorStackForChunk` | 6351 | Chunk → does its slot have an operator stack? True for any in-layout slot with a definition whose archetype is one of the 8 (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater); false outside the layout. **That list is written down here and nowhere else.** `ClassifyTile` guards on it; `GetDensityAt` uses it to share TunnelNetwork stacks by chunk Z. |
| `GetRecipeForChunk` | — | Runtime immutable recipe/vector/strate-seed copy for cooked seasons; editor slot overrides use the same worker hand-off. |
| `GetSlabParamsForChunk` | 6263 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
| `GetSlabParamsForChunk` | 6415 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
| `GetGenerationParams` | 6509 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
| `GetGenerationParams` | 6661 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
| `SetComposerOverrideForStrate` / `GetComposerOverrideForChunk` | editor-only | Temporary density-only candidate overlay for one existing slot. Copies immutable params/recipe into the worker refetch path, leaves layout/content/passages intact, and bumps `PassagesVersion` to invalidate generator memos. |
| `BuildParamsFromDefinition` (static) | 6815 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
| `BuildParamsFromDefinition` (static) | 6985 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
@@ -107,6 +107,14 @@ std::int64_t WorldAssemblyRoundRatio(std::int64_t Numerator, std::int64_t Denomi
return -((-Numerator + Denominator / 2) / Denominator);
}
std::int64_t WorldAssemblyMicrometresToSubvoxels(
std::int64_t Micrometres, std::uint32_t CoreVoxelMicrometres)
{
if (CoreVoxelMicrometres == 0) return 0;
return WorldAssemblyRoundRatio(Micrometres * WorldAssemblySubvoxelsPerVoxel,
static_cast<std::int64_t>(CoreVoxelMicrometres));
}
std::uint64_t WorldAssemblyRoundedSqrt(std::uint64_t Value)
{
std::uint64_t Root = 0;
@@ -312,17 +320,18 @@ VoxelForgeCore::WorldAssemblyBiomeProfileV1 BuildSurfaceProfile(
bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
const FStrateSlot& Slot,
VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum, FString& OutError)
VoxelForgeCore::WorldAssemblyStratumPlanV1& Stratum,
std::uint32_t CoreVoxelMicrometres, FString& OutError)
{
using namespace VoxelForgeCore;
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
const FSurfaceGenerationParams Surface = StrateManager->GetSurfaceParamsForChunk(Chunk);
const FBiomeContext BiomeContext = StrateManager->GetBiomeContextForChunk(Chunk);
const UVoxelStrateDefinition* Definition = Slot.Definition;
const double LayoutChunkVoxels = static_cast<double>(Recipe.layoutChunkWidthSubvoxels)
/ WorldAssemblySubvoxelsPerVoxel;
const double BottomVoxel = static_cast<double>(Slot.BottomChunkZ) * LayoutChunkVoxels;
const double HeightVoxels = static_cast<double>(Slot.TopChunkZ - Slot.BottomChunkZ + 1) * LayoutChunkVoxels;
const double BottomVoxel = static_cast<double>(Slot.BottomWorldMicrometres)
/ std::max<std::uint32_t>(1, CoreVoxelMicrometres);
const double HeightVoxels = static_cast<double>(Slot.HeightMicrometres)
/ std::max<std::uint32_t>(1, CoreVoxelMicrometres);
const FBiomeMapParams& Map = BiomeContext.Map;
Recipe.seed = static_cast<std::uint32_t>(StrateManager->GetWorldSeed());
Recipe.biomeCellSizeSubvoxels = WorldAssemblyFixed(Map.CellSize);
@@ -454,9 +463,12 @@ bool BuildSurfaceStratumPlan(const UVoxelStrateManager* StrateManager,
return false;
}
Shelf.minimumSubvoxels = {WorldAssemblyFixed(MinX), WorldAssemblyFixed(MinY),
std::max(WorldAssemblyFixed(BottomVoxel), LowestGround - WorldAssemblyFixed(4.0))};
std::max(WorldAssemblyMicrometresToSubvoxels(
Slot.BottomWorldMicrometres, CoreVoxelMicrometres),
LowestGround - WorldAssemblyFixed(4.0))};
Shelf.maximumSubvoxels = {WorldAssemblyFixed(MaxX), WorldAssemblyFixed(MaxY),
std::min(WorldAssemblyFixed((Slot.TopChunkZ + 1) * LayoutChunkVoxels),
std::min(WorldAssemblyMicrometresToSubvoxels(
Slot.TopWorldMicrometres, CoreVoxelMicrometres),
HighestGround + WorldAssemblyFixed(ShelfHeight + 4.0))};
Shelf.parametersSubvoxels = {WorldAssemblyFixed(DirX), WorldAssemblyFixed(DirY),
WorldAssemblyFixed(ShelfHeight), WorldAssemblyFixed(Reach)};
@@ -479,7 +491,7 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
using namespace VoxelForgeCore;
const FIntVector Chunk(0, 0, Slot.TopChunkZ);
const FFloatingIslandParams Params = StrateManager->GetFloatingIslandParamsForChunk(Chunk);
const FStratePassageConfig& Passage = Slot.Definition->PassageConfig;
const FStratePassageConfig& Passage = Slot.ResolvedPassageConfig;
WorldAssemblyFloatingIslandPlanV1 IslandPlan;
IslandPlan.strateIndex = Slot.StrateIndex;
IslandPlan.topChunkZ = Slot.TopChunkZ;
@@ -500,10 +512,10 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
}
Recipe.ownerToCoreScaleNumerator = Recipe.ownerVoxelSizeMicrons / ScaleGcd;
Recipe.ownerToCoreScaleDenominator = Recipe.coreVoxelSizeMicrons / ScaleGcd;
Recipe.layoutMinimumZSubvoxels = static_cast<std::int64_t>(Slot.BottomChunkZ)
* LayoutChunkWidthSubvoxels;
Recipe.layoutMaximumZSubvoxels = static_cast<std::int64_t>(Slot.TopChunkZ + 1)
* LayoutChunkWidthSubvoxels;
Recipe.layoutMinimumZSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.BottomWorldMicrometres, Recipe.coreVoxelSizeMicrons);
Recipe.layoutMaximumZSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.TopWorldMicrometres, Recipe.coreVoxelSizeMicrons);
Recipe.layoutCenterTopZSubvoxels = (Recipe.layoutMinimumZSubvoxels
+ Recipe.layoutMaximumZSubvoxels) / 2;
Recipe.islandDensityQ16 = WorldAssemblyQ16(FMath::Clamp(Params.IslandDensity, 0.0f, 1.0f));
@@ -524,20 +536,35 @@ bool BuildFloatingIslandStratumPlan(const UVoxelStrateManager* StrateManager,
*WorldAssemblySubvoxelsPerVoxel/Recipe.coreVoxelSizeMicrons));
};
Recipe.anchorIslandOrdinal=Recipe.primaryIslandCount-1;
Recipe.anchorPlacementRadiusSubvoxels=OwnerVoxelsToCoreQ(Passage.DistanceMax);
const std::int64_t spiralRadius=OwnerVoxelsToCoreQ(Passage.SpiralRadius);
Recipe.anchorPlacementRadiusSubvoxels=WorldAssemblyMicrometresToSubvoxels(
Slot.PassageDistanceMaximumMicrometres, Recipe.coreVoxelSizeMicrons);
const std::int64_t spiralRadius=WorldAssemblyMicrometresToSubvoxels(
Slot.PassageSpiralRadiusMicrometres, Recipe.coreVoxelSizeMicrons);
// Reserve at least a 1.5 m mid-bore through the anchor root.
constexpr double AnchorPassageMinimumBoreOwnerVoxels=12.0;
const std::int64_t mouthBore=std::max(OwnerVoxelsToCoreQ(Passage.MouthRadius),
OwnerVoxelsToCoreQ(12.0));
const std::int64_t bore=std::max(OwnerVoxelsToCoreQ(Passage.MidRadius),
OwnerVoxelsToCoreQ(AnchorPassageMinimumBoreOwnerVoxels));
const std::int64_t wallReserve=OwnerVoxelsToCoreQ(12.0);
Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+OwnerVoxelsToCoreQ(4.0);
const std::int64_t excursion=std::max(OwnerVoxelsToCoreQ(Passage.Wander),spiralRadius);
const std::int64_t OwnerVoxelMicrometres = Recipe.ownerVoxelSizeMicrons;
const std::int64_t mouthBore=std::max(
WorldAssemblyMicrometresToSubvoxels(Slot.PassageMouthRadiusMicrometres,
Recipe.coreVoxelSizeMicrons),
WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres,
Recipe.coreVoxelSizeMicrons));
const std::int64_t bore=std::max(
WorldAssemblyMicrometresToSubvoxels(Slot.PassageMidRadiusMicrometres,
Recipe.coreVoxelSizeMicrons),
WorldAssemblyMicrometresToSubvoxels(
static_cast<std::int64_t>(AnchorPassageMinimumBoreOwnerVoxels * OwnerVoxelMicrometres),
Recipe.coreVoxelSizeMicrons));
const std::int64_t wallReserve=WorldAssemblyMicrometresToSubvoxels(
12 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons);
const std::int64_t FourOwnerVoxelsSubvoxels=WorldAssemblyMicrometresToSubvoxels(
4 * OwnerVoxelMicrometres, Recipe.coreVoxelSizeMicrons);
Recipe.anchorMouthOffsetSubvoxels=spiralRadius+mouthBore+FourOwnerVoxelsSubvoxels;
const std::int64_t excursion=std::max(
WorldAssemblyMicrometresToSubvoxels(Slot.PassageWanderMicrometres,
Recipe.coreVoxelSizeMicrons), spiralRadius);
Recipe.anchorRootRadiusSubvoxels=excursion+bore+wallReserve;
Recipe.anchorIslandMinimumRadiusSubvoxels=std::max(Recipe.minimumRadiusSubvoxels,
Recipe.anchorMouthOffsetSubvoxels+mouthBore+OwnerVoxelsToCoreQ(4.0));
Recipe.anchorMouthOffsetSubvoxels+mouthBore+FourOwnerVoxelsSubvoxels);
Recipe.anchorRootBottomZSubvoxels=Recipe.layoutMinimumZSubvoxels
+Recipe.boundarySealSubvoxels;
std::string PlanError;
@@ -882,7 +909,7 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
SlotRecipe.layoutChunkWidthSubvoxels=LayoutChunkWidthSubvoxels;
VoxelForgeCore::WorldAssemblyStratumPlanV1 Stratum;
if (!BuildSurfaceStratumPlan(StrateManager, Slot,
SlotRecipe, Stratum, OutError))
SlotRecipe, Stratum, CoreVoxelMicrometres, OutError))
{
return false;
}
@@ -1048,41 +1075,47 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
return "Unknown";
};
const std::int64_t OwnerVoxelMicrons = Layout.ownerVoxelSizeMicrometres;
const auto FixedToMicrons = [ExpectedCoreVoxelMicrons](std::int64_t Fixed)
{
return static_cast<std::int64_t>(std::llround(static_cast<double>(Fixed)
* ExpectedCoreVoxelMicrons / VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
};
std::vector<VoxelForgeCore::ResolvedLayoutPassageConfigV1> PassageConfigs;
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (Slot.Definition == nullptr) continue;
const FStratePassageConfig& AuthoredPassage = Slot.Definition->PassageConfig;
const auto OwnerVoxelsToCoreQ = [OwnerVoxelMicrons, ExpectedCoreVoxelMicrons](double Value)
{
return static_cast<std::int64_t>(std::llround(Value * OwnerVoxelMicrons
* VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel / ExpectedCoreVoxelMicrons));
};
const FStratePassageConfig& ResolvedPassage = Slot.ResolvedPassageConfig;
VoxelForgeCore::ResolvedLayoutPassageConfigV1 PassageConfig;
PassageConfig.upperStrateIndex = Slot.StrateIndex;
PassageConfig.connections = AuthoredPassage.Connections;
PassageConfig.style = static_cast<std::uint32_t>(AuthoredPassage.Style);
PassageConfig.mouthRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(AuthoredPassage.MouthRadius),
OwnerVoxelsToCoreQ(12.0));
PassageConfig.midRadiusSubvoxels = std::max(OwnerVoxelsToCoreQ(AuthoredPassage.MidRadius),
OwnerVoxelsToCoreQ(12.0));
PassageConfig.reachMinimumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.ReachMin);
PassageConfig.reachMaximumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.ReachMax);
PassageConfig.distanceMinimumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.DistanceMin);
PassageConfig.distanceMaximumSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.DistanceMax);
PassageConfig.wanderSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.Wander);
PassageConfig.segments = static_cast<std::uint32_t>(FMath::Clamp(AuthoredPassage.Segments,1,48));
PassageConfig.verticalWobbleSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.VerticalWobble);
PassageConfig.spiralRadiusSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.SpiralRadius);
PassageConfig.connections = ResolvedPassage.Connections;
PassageConfig.style = static_cast<std::uint32_t>(ResolvedPassage.Style);
PassageConfig.mouthRadiusSubvoxels = std::max(
WorldAssemblyMicrometresToSubvoxels(
Slot.PassageMouthRadiusMicrometres, ExpectedCoreVoxelMicrons),
WorldAssemblyMicrometresToSubvoxels(
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
PassageConfig.midRadiusSubvoxels = std::max(
WorldAssemblyMicrometresToSubvoxels(
Slot.PassageMidRadiusMicrometres, ExpectedCoreVoxelMicrons),
WorldAssemblyMicrometresToSubvoxels(
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
PassageConfig.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageReachMinimumMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageReachMaximumMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.distanceMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageDistanceMinimumMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.distanceMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageDistanceMaximumMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.wanderSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageWanderMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.segments = static_cast<std::uint32_t>(
FMath::Clamp(ResolvedPassage.Segments, 1, 48));
PassageConfig.verticalWobbleSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageVerticalWobbleMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.spiralRadiusSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageSpiralRadiusMicrometres, ExpectedCoreVoxelMicrons);
PassageConfig.spiralTurnsQ16 = static_cast<std::int64_t>(std::llround(
static_cast<double>(AuthoredPassage.SpiralTurns)*65536.0));
PassageConfig.cascadeSteps = static_cast<std::uint32_t>(FMath::Clamp(AuthoredPassage.CascadeSteps,1,16));
PassageConfig.cascadeLedgeSubvoxels = OwnerVoxelsToCoreQ(AuthoredPassage.CascadeLedge);
static_cast<double>(ResolvedPassage.SpiralTurns) * 65536.0));
PassageConfig.cascadeSteps = static_cast<std::uint32_t>(
FMath::Clamp(ResolvedPassage.CascadeSteps, 1, 16));
PassageConfig.cascadeLedgeSubvoxels = WorldAssemblyMicrometresToSubvoxels(
Slot.PassageCascadeLedgeMicrometres, ExpectedCoreVoxelMicrons);
PassageConfigs.push_back(PassageConfig);
const ECaveGeneratorType Type = StrateManager->GetGeneratorTypeForChunk(
FIntVector(0, 0, Slot.TopChunkZ));
@@ -1096,16 +1129,13 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
: std::string("legacy.") + generatorName(Type);
Item.recipeVersion = Type == ECaveGeneratorType::SurfaceWorld ? Manifest->recipe.schemaVersion : 0;
Item.voxelSizeMicrometres = ExpectedCoreVoxelMicrons;
Item.interiorBottomMicrometres = static_cast<std::int64_t>(Slot.BottomChunkZ)
* LayoutChunkSizeMicrometres;
Item.interiorTopMicrometres = static_cast<std::int64_t>(Slot.TopChunkZ + 1)
* LayoutChunkSizeMicrometres;
Item.interiorBottomMicrometres = Slot.BottomWorldMicrometres;
Item.interiorTopMicrometres = Slot.TopWorldMicrometres;
Item.managerChunkWidthMicrometres = LayoutChunkSizeMicrometres;
Item.dispatchChunkWidthMicrometres = LayoutChunkSizeMicrometres;
Item.transitionMode = transitionName(Slot.Definition->TransitionType);
Item.transitionBlendChunks = Slot.Definition->TransitionBlendChunks;
Item.transitionWidthMicrometres = static_cast<std::int64_t>(Item.transitionBlendChunks)
* Item.managerChunkWidthMicrometres;
Item.transitionMode = transitionName(Slot.ResolvedTransitionType);
Item.transitionBlendChunks = Slot.ResolvedTransitionBlendChunks;
Item.transitionWidthMicrometres = Slot.TransitionWidthMicrometres;
Item.transitionWarpAmplitudeMicrometres = Item.transitionMode == "Interleaved"
? std::min(Item.transitionWidthMicrometres / 2, Item.managerChunkWidthMicrometres) : 0;
Item.transitionInterpenetrationMicrometres = Item.transitionMode == "Interleaved"
@@ -1161,11 +1191,8 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
Item.queryDispatchPath = "NativeFloatingIslandDensityAt selects floor(CoreZ / resolved layout chunk width) from ResolvedLayoutSnapshotV3";
const auto Plan = std::find_if(Manifest->floatingIslands.begin(), Manifest->floatingIslands.end(),
[&Slot](const auto& Entry) { return Entry.strateIndex == Slot.StrateIndex; });
std::int64_t SealMicrometres = 0;
if (Plan != Manifest->floatingIslands.end())
{
Item.interiorBottomMicrometres = FixedToMicrons(Plan->manifest.recipe.layoutMinimumZSubvoxels);
Item.interiorTopMicrometres = FixedToMicrons(Plan->manifest.recipe.layoutMaximumZSubvoxels);
Item.recipeVersion = Plan->manifest.recipe.geometryVersion;
for (std::size_t IslandIndex = 0; IslandIndex < Plan->manifest.plan.islands.size(); ++IslandIndex)
{
@@ -1315,6 +1342,8 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
}
const int32 Seed = StrateManager->GetWorldSeed();
const std::uint32_t LayoutChunkSizeMicrometres =
StrateManager->GetStrateLayoutChunkSizeMicrometres();
const float OriginSpineRadius = Settings->GetEffectiveOriginSpineRadius();
const float WorldRadiusVoxels = Settings->GetEffectiveWorldRadiusVoxels();
const int32 CoreClipRadius = FMath::Clamp(ClipRadius, 1, 64);
@@ -1332,7 +1361,7 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
SettingsValue->AsObject()->SetNumberField(TEXT("ClipRadius"), CoreClipRadius);
SettingsValue->AsObject()->SetNumberField(TEXT("MaxClipLevel"), CoreMaxClipLevel);
SettingsValue->AsObject()->SetNumberField(TEXT("StrateLayoutChunkSizeMeters"),
Settings->StrateLayoutChunkSizeMeters);
static_cast<double>(StrateManager->GetStrateLayoutChunkSizeMicrometres()) / 1000000.0);
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetNumberField(TEXT("schema_version"), 1);
@@ -1343,17 +1372,20 @@ bool VoxelForgeCoreAdapter::SerializeResolvedWorld(
static_cast<std::uint32_t>(std::llround(OwnerVoxelSizeCm * 10000.0)));
Root->SetNumberField(TEXT("core_voxel_size_cm"), ResolvedCoreVoxelSizeCm);
Root->SetNumberField(TEXT("core_voxel_size_microns"), CoreVoxelSizeMicrons);
Root->SetNumberField(TEXT("owner_to_core_length_scale"),
OwnerVoxelSizeCm / ResolvedCoreVoxelSizeCm);
Root->SetNumberField(TEXT("seed"), Seed);
Root->SetBoolField(TEXT("open_surface_entry"), Settings->bOpenSurfaceEntry);
Root->SetNumberField(TEXT("origin_spine_radius"), OriginSpineRadius);
Root->SetNumberField(TEXT("world_radius_voxels"), WorldRadiusVoxels);
Root->SetNumberField(TEXT("edge_seal_thickness"), Settings->EdgeSealThickness);
Root->SetNumberField(TEXT("total_strates"), StrateManager->GetNumStrates());
Root->SetNumberField(TEXT("inter_strate_gap_chunks"), Settings->InterStrateGapChunks);
const std::int64_t ResolvedGapMicrometres =
StrateManager->GetResolvedInterStrateGapMicrometres();
Root->SetNumberField(TEXT("inter_strate_gap_chunks"),
LayoutChunkSizeMicrometres > 0
? ResolvedGapMicrometres / LayoutChunkSizeMicrometres : 0);
Root->SetNumberField(TEXT("inter_strate_gap_um"), ResolvedGapMicrometres);
Root->SetNumberField(TEXT("strate_layout_chunk_size_um"),
std::llround(Settings->StrateLayoutChunkSizeMeters*1000000.0));
LayoutChunkSizeMicrometres);
Root->SetNumberField(TEXT("clip_radius"), CoreClipRadius);
Root->SetNumberField(TEXT("max_clip_level"), CoreMaxClipLevel);
Root->SetNumberField(TEXT("full_res_clip_levels"), Settings->FullResClipLevels);
+218 -91
View File
@@ -25,6 +25,27 @@ namespace
{
std::atomic<uint64> GNextStrateManagerLifetimeId { 0 };
int64 VF_OwnerVoxelsToMicrometres(double OwnerVoxels)
{
return static_cast<int64>(std::llround(
OwnerVoxels * static_cast<double>(VOXEL_SIZE) * 10000.0));
}
float VF_ResolvedWorldMicrometresToOwnerVoxels(int64 WorldMicrometres)
{
return static_cast<float>(static_cast<double>(WorldMicrometres)
/ (static_cast<double>(VOXEL_SIZE) * 10000.0));
}
int64 VF_FloorDivide64(int64 Numerator, int64 Denominator)
{
if (Denominator <= 0) return 0;
int64 Quotient = Numerator / Denominator;
const int64 Remainder = Numerator % Denominator;
if (Remainder < 0) --Quotient;
return Quotient;
}
// Exact A/B switch for the repeated passage-floor projection item. The other field-
// preserving rewrites in this round are deliberately unconditional; this switch isolates
// the projection reuse without changing any density arithmetic.
@@ -1301,8 +1322,11 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
}
}
// Current Z position (in chunks). Starts at 0 and goes downward (negative).
int32 CurrentTopZ = 0;
// Resolve the stack in physical integer micrometres first. The historical chunk indices are
// projected from these half-open bounds only after each slot is complete.
int64 CurrentTopWorldMicrometres = StrateLayoutChunkSizeMicrometres;
const int64 ResolvedGapMicrometres = static_cast<int64>(LayoutGapChunks)
* StrateLayoutChunkSizeMicrometres;
for (int32 i = 0; i < TotalStrates; i++)
{
@@ -1357,16 +1381,28 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
const int32 LayoutChunkVoxels=FMath::RoundToInt(StrateLayoutChunkWidthVoxels);
Slot.TopChunkZ = SeasonStrate.TopWorldZ / LayoutChunkVoxels - 1;
Slot.BottomChunkZ = SeasonStrate.BottomWorldZ / LayoutChunkVoxels;
Slot.TopWorldMicrometres = (static_cast<int64>(Slot.TopChunkZ) + 1)
* StrateLayoutChunkSizeMicrometres;
Slot.BottomWorldMicrometres = static_cast<int64>(Slot.BottomChunkZ)
* StrateLayoutChunkSizeMicrometres;
Slot.HeightMicrometres = Slot.TopWorldMicrometres - Slot.BottomWorldMicrometres;
const int64 ExpectedTopChunk = VF_FloorDivide64(
CurrentTopWorldMicrometres, StrateLayoutChunkSizeMicrometres) - 1;
const int64 ExpectedBottomChunk = ExpectedTopChunk - (Slot.HeightInChunks - 1);
if (SeasonStrate.TopWorldZ % LayoutChunkVoxels != 0
|| SeasonStrate.BottomWorldZ % LayoutChunkVoxels != 0
|| Slot.TopChunkZ != CurrentTopZ
|| Slot.BottomChunkZ != CurrentTopZ - (Slot.HeightInChunks - 1))
|| Slot.TopChunkZ != ExpectedTopChunk
|| Slot.BottomChunkZ != ExpectedBottomChunk
|| Slot.TopWorldMicrometres != CurrentTopWorldMicrometres
|| Slot.HeightMicrometres != static_cast<int64>(Slot.HeightInChunks)
* StrateLayoutChunkSizeMicrometres)
{
UE_LOG(LogTemp, Error,
TEXT("[StrateManager] Season slot %d bounds do not describe the declared stacked layout."), i);
RestorePreviousLayout();
return false;
}
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
SeasonStrates.Add(SeasonStrate);
}
else
@@ -1389,15 +1425,50 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
continue;
}
// Compute Z range from definition's height
// Resolve the authored chunk height into an exact physical interval.
Slot.HeightInChunks = Slot.Definition->StrateHeightInChunks;
Slot.TopChunkZ = CurrentTopZ;
Slot.BottomChunkZ = CurrentTopZ - (Slot.HeightInChunks - 1);
Slot.TopWorldMicrometres = CurrentTopWorldMicrometres;
Slot.HeightMicrometres = static_cast<int64>(Slot.HeightInChunks)
* StrateLayoutChunkSizeMicrometres;
Slot.BottomWorldMicrometres = Slot.TopWorldMicrometres - Slot.HeightMicrometres;
Slot.TopChunkZ = static_cast<int32>(VF_FloorDivide64(
Slot.TopWorldMicrometres, StrateLayoutChunkSizeMicrometres) - 1);
Slot.BottomChunkZ = static_cast<int32>(VF_FloorDivide64(
Slot.BottomWorldMicrometres, StrateLayoutChunkSizeMicrometres));
CurrentTopWorldMicrometres = Slot.BottomWorldMicrometres - ResolvedGapMicrometres;
}
// Move the cursor down for the next strate, leaving a solid-bedrock gap of
// InterStrateGapChunks chunks between this strate and the next.
CurrentTopZ = Slot.BottomChunkZ - 1 - LayoutGapChunks;
Slot.ResolvedTransitionType = Slot.Definition->TransitionType;
Slot.ResolvedTransitionBlendChunks = Slot.Definition->TransitionBlendChunks;
Slot.TransitionWidthMicrometres = static_cast<int64>(
Slot.ResolvedTransitionBlendChunks) * StrateLayoutChunkSizeMicrometres;
Slot.ResolvedPassageConfig = Slot.Definition->PassageConfig;
Slot.PassageMouthRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.MouthRadius);
Slot.PassageMidRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.MidRadius);
Slot.PassageReachMinimumMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.ReachMin);
Slot.PassageReachMaximumMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.ReachMax);
Slot.PassageDistanceMinimumMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.DistanceMin);
Slot.PassageDistanceMaximumMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.DistanceMax);
Slot.PassageWanderMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.Wander);
Slot.PassageVerticalWobbleMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.VerticalWobble);
Slot.PassageSpiralRadiusMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.SpiralRadius);
Slot.PassageCascadeLedgeMicrometres = VF_OwnerVoxelsToMicrometres(
Slot.ResolvedPassageConfig.CascadeLedge);
Slot.LegacyTopWorldZVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
Slot.TopWorldMicrometres);
Slot.LegacyBottomWorldZVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
Slot.BottomWorldMicrometres);
Slot.LegacyHeightVoxels = VF_ResolvedWorldMicrometresToOwnerVoxels(
Slot.HeightMicrometres);
StrateLayout.Add(Slot);
@@ -1430,6 +1501,7 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
OriginSpineRadius = bUseSeason
? SeasonManifest.OriginSpineRadius : Settings->OriginSpineRadius;
InterStrateGapChunks = LayoutGapChunks;
ResolvedInterStrateGapMicrometres = ResolvedGapMicrometres;
if (bUseSeason)
{
ActiveSeasonContentHash = SeasonManifest.ContentHash;
@@ -1538,8 +1610,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
}
VF_SetComposerRuntimeBounds(
Override->Params,
(float)(TargetSlot->TopChunkZ + 1) * StrateLayoutChunkWidthVoxels,
(float)TargetSlot->BottomChunkZ * StrateLayoutChunkWidthVoxels);
TargetSlot->LegacyTopWorldZVoxels,
TargetSlot->LegacyBottomWorldZVoxels);
Override->bUseRecipe = bUseRecipe;
if (Recipe != nullptr)
{
@@ -1558,8 +1630,8 @@ bool UVoxelStrateManager::SetComposerOverrideForStrate(
Override->bUseRegions = true;
Override->Regions = *InRegions;
VF_RekeyStrateRegionManifest(Override->Regions, CandidateSeed, StrateIndex);
Override->Regions.StrateTopWorldZ = (float)(TargetSlot->TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Override->Regions.StrateBottomWorldZ = (float)TargetSlot->BottomChunkZ * StrateLayoutChunkWidthVoxels;
Override->Regions.StrateTopWorldZ = TargetSlot->LegacyTopWorldZVoxels;
Override->Regions.StrateBottomWorldZ = TargetSlot->LegacyBottomWorldZVoxels;
VF_SetStrateArchetypeRuntimeBounds(Override->Params,
Override->Regions.StrateTopWorldZ,
Override->Regions.StrateBottomWorldZ);
@@ -1806,14 +1878,36 @@ void UVoxelStrateManager::GeneratePassages()
// that opens the next room rather than a second vertical spine.
const UVoxelStrateDefinition* UpperDef = Upper.Definition;
if (!UpperDef) continue;
const FStratePassageConfig& Cfg = UpperDef->PassageConfig;
// GeneratePassages is the legacy owner-voxel route adapter. Its immutable source is the
// physical passage record resolved during Initialize, converted back to owner voxels here.
FStratePassageConfig Cfg = Upper.ResolvedPassageConfig;
Cfg.MouthRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageMouthRadiusMicrometres);
Cfg.MidRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageMidRadiusMicrometres);
Cfg.ReachMin = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageReachMinimumMicrometres);
Cfg.ReachMax = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageReachMaximumMicrometres);
Cfg.DistanceMin = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageDistanceMinimumMicrometres);
Cfg.DistanceMax = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageDistanceMaximumMicrometres);
Cfg.Wander = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageWanderMicrometres);
Cfg.VerticalWobble = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageVerticalWobbleMicrometres);
Cfg.SpiralRadius = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageSpiralRadiusMicrometres);
Cfg.CascadeLedge = VF_ResolvedWorldMicrometresToOwnerVoxels(
Upper.PassageCascadeLedgeMicrometres);
// Upper strate floor and lower strate ceiling (differ when there's a bedrock gap).
const float UpperTopZ = (float)(Upper.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
const float UpperBottomZ = (float)(Upper.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float LowerTopZ = (float)(Lower.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
const float UpperMax = (float)Upper.HeightInChunks * StrateLayoutChunkWidthVoxels * 0.9f;
const float LowerMax = (float)Lower.HeightInChunks * StrateLayoutChunkWidthVoxels * 0.9f;
const float UpperTopZ = Upper.LegacyTopWorldZVoxels;
const float UpperBottomZ = Upper.LegacyBottomWorldZVoxels;
const float LowerTopZ = Lower.LegacyTopWorldZVoxels;
const float UpperMax = Upper.LegacyHeightVoxels * 0.9f;
const float LowerMax = Lower.LegacyHeightVoxels * 0.9f;
const float DistLo = FMath::Min(Cfg.DistanceMin, Cfg.DistanceMax);
const float DistHi = FMath::Max(Cfg.DistanceMin, Cfg.DistanceMax);
@@ -1943,7 +2037,7 @@ void UVoxelStrateManager::GeneratePassages()
LowerDef->FloatingIslandParams,
LowerQuerySeed,
LowerTopZ,
(float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels,
Lower.LegacyBottomWorldZVoxels,
PX,
PY,
MaxLateralSnap,
@@ -2003,7 +2097,7 @@ void UVoxelStrateManager::GeneratePassages()
if (bAimedLowerAtPlayerFitPoint)
{
const float LowerSealThickness = BoundarySealThicknessFor(*LowerDef);
const float LowerBottomZ = (float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float LowerBottomZ = Lower.LegacyBottomWorldZVoxels;
const float InnerBottomZ = LowerBottomZ + LowerSealThickness;
const float InnerTopZ = LowerTopZ - LowerSealThickness;
if (VoxelMath::IsFinite(SuggestedLowerPoint.X)
@@ -2171,7 +2265,7 @@ void UVoxelStrateManager::GeneratePassages()
Cfg.MouthRadius,
LowerDoorDirection,
LowerTopZ,
(float)(Lower.BottomChunkZ) * StrateLayoutChunkWidthVoxels,
Lower.LegacyBottomWorldZVoxels,
LowerDef ? BoundarySealThicknessFor(*LowerDef) : 0.0f,
bAimedLowerAtPlayerFitPoint);
Passage.UpperPoint = Passage.UpperLanding.StandingPoint;
@@ -2596,8 +2690,8 @@ void UVoxelStrateManager::GeneratePassages()
if (bOpenSurfaceEntry && OriginSpineRadius > 0.0f && StrateLayout.Num() > 0)
{
const FStrateSlot& Top = StrateLayout[0];
const float TopZ = (float)(Top.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
const float BottomZ = (float)Top.BottomChunkZ * StrateLayoutChunkWidthVoxels;
const float TopZ = Top.LegacyTopWorldZVoxels;
const float BottomZ = Top.LegacyBottomWorldZVoxels;
const float TopSeal = Top.Definition
? BoundarySealThicknessFor(*Top.Definition) : 0.0f;
const VoxelPassageGeometry::FOriginLandingGeometry OriginLanding =
@@ -5922,8 +6016,8 @@ bool UVoxelStrateManager::AnyLandingFloorAtLattice(
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float TopZ = Slot.LegacyTopWorldZVoxels;
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
@@ -5965,8 +6059,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearBox(
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float TopZ = Slot.LegacyTopWorldZVoxels;
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
if (VoxelPassageGeometry::OriginLandingRoomTouchesBox(
VoxelBox, TopZ, BottomZ,
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
@@ -5986,8 +6080,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearBox(
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float TopZ = Slot.LegacyTopWorldZVoxels;
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
if (VoxelPassageGeometry::OriginLandingFloorTouchesBox(
VoxelBox, TopZ, BottomZ,
VF_BoundarySealThicknessForDefinition(*Slot.Definition),
@@ -6011,8 +6105,8 @@ bool UVoxelStrateManager::AnyOriginLandingNearLattice(
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float TopZ = Slot.LegacyTopWorldZVoxels;
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
if (VoxelPassageGeometry::OriginLandingRoomTouchesLattice(
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius,
@@ -6049,8 +6143,8 @@ bool UVoxelStrateManager::AnyOriginLandingAirNearLattice(
{
continue;
}
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float TopZ = Slot.LegacyTopWorldZVoxels;
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
const VoxelPassageGeometry::FOriginLandingGeometry Geometry =
VoxelPassageGeometry::BuildOriginLandingGeometry(
@@ -6126,8 +6220,8 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr) continue;
const float TopZ = (static_cast<float>(Slot.TopChunkZ) + 1.0f) * StrateLayoutChunkWidthVoxels;
const float BottomZ = static_cast<float>(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float TopZ = Slot.LegacyTopWorldZVoxels;
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
const float Seal = VF_BoundarySealThicknessForDefinition(*Slot.Definition);
if (VoxelPassageGeometry::OriginLandingFloorTouchesLattice(
VoxelBox, LatticeOrigin, Step, TopZ, BottomZ, Seal, OriginSpineRadius))
@@ -6144,15 +6238,36 @@ bool UVoxelStrateManager::AnyOriginLandingFloorNearLattice(
int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
{
// Linear search through strate layout.
// With ~10-20 strates this is fine. If we ever have hundreds,
// switch to binary search (layout is sorted by Z).
for (int32 i = 0; i < StrateLayout.Num(); i++)
// Legacy chunk callers use the projected interval cached during Initialize. The source bounds
// are the slot's physical micrometre interval; these indices do not define or stack strates.
for (int32 i = 0; i < StrateLayout.Num(); ++i)
{
const FStrateSlot& Slot = StrateLayout[i];
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ)
if (ChunkZ <= Slot.TopChunkZ && ChunkZ >= Slot.BottomChunkZ) return i;
}
return -1;
}
int32 UVoxelStrateManager::FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const
{
const double WorldZMicrometres = WorldZCentimetres * 10000.0;
int32 Low = 0;
int32 High = StrateLayout.Num() - 1;
while (Low <= High)
{
const int32 Mid = Low + (High - Low) / 2;
const FStrateSlot& Slot = StrateLayout[Mid];
if (WorldZMicrometres >= static_cast<double>(Slot.TopWorldMicrometres))
{
return i;
High = Mid - 1;
}
else if (WorldZMicrometres < static_cast<double>(Slot.BottomWorldMicrometres))
{
Low = Mid + 1;
}
else
{
return Mid;
}
}
return -1;
@@ -6160,9 +6275,8 @@ int32 UVoxelStrateManager::FindSlotIndexForChunkZ(int32 ChunkZ) const
UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
{
// Convert world Z to chunk Z coordinate
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
if (!FMath::IsFinite(WorldZ)) return nullptr;
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
if (SlotIdx >= 0)
{
return StrateLayout[SlotIdx].Definition;
@@ -6172,8 +6286,8 @@ UVoxelStrateDefinition* UVoxelStrateManager::GetStrateAt(float WorldZ) const
int32 UVoxelStrateManager::GetStrateIndex(float WorldZ) const
{
int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
if (!FMath::IsFinite(WorldZ)) return -1;
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
if (SlotIdx >= 0)
{
return StrateLayout[SlotIdx].StrateIndex;
@@ -6286,9 +6400,13 @@ bool UVoxelStrateManager::IsGapChunk(const FIntVector& ChunkCoord) const
if (StrateLayout.Num() == 0) return false;
// Above the top strate or below the bottom strate = open air, NOT a gap.
const int32 StackTop = StrateLayout[0].TopChunkZ;
const int32 StackBottom = StrateLayout.Last().BottomChunkZ;
if (ChunkCoord.Z > StackTop || ChunkCoord.Z < StackBottom) return false;
const int64 ChunkCenterMicrometres = static_cast<int64>(ChunkCoord.Z)
* StrateLayoutChunkSizeMicrometres + StrateLayoutChunkSizeMicrometres / 2;
if (ChunkCenterMicrometres >= StrateLayout[0].TopWorldMicrometres
|| ChunkCenterMicrometres < StrateLayout.Last().BottomWorldMicrometres)
{
return false;
}
// Inside the stack's Z span but not in any strate slot → it's a bedrock gap.
return FindSlotIndexForChunkZ(ChunkCoord.Z) < 0;
@@ -6332,8 +6450,8 @@ FSlabGenerationParams UVoxelStrateManager::GetSlabParamsForChunk(const FIntVecto
// Fill in the runtime Z bounds (voxel coordinates, same convention as
// FStrateGenerationParams::StrateTopWorldZ / StrateBottomWorldZ).
// TopChunkZ+1 because the top chunk's CEILING is at (TopChunkZ+1)*StrateLayoutChunkWidthVoxels.
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
return Result;
}
@@ -6366,8 +6484,8 @@ FMazeGenerationParams UVoxelStrateManager::GetMazeParamsForChunk(const FIntVecto
}
}
#endif
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
return Result;
}
@@ -6393,8 +6511,8 @@ FSurfaceGenerationParams UVoxelStrateManager::GetSurfaceParamsForChunk(const FIn
}
}
#endif
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
return Result;
}
@@ -6420,8 +6538,8 @@ FVerticalShaftParams UVoxelStrateManager::GetVerticalShaftParamsForChunk(const F
}
}
#endif
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
return Result;
}
@@ -6447,8 +6565,8 @@ FFloatingIslandParams UVoxelStrateManager::GetFloatingIslandParamsForChunk(const
}
}
#endif
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
return Result;
}
@@ -6482,14 +6600,14 @@ FBiomeContext UVoxelStrateManager::GetBiomeContextForChunk(const FIntVector& Chu
bool UVoxelStrateManager::GetStrateUnrealZRange(float WorldZ, float& OutTopZ, float& OutBottomZ) const
{
const int32 ChunkZ = FMath::FloorToInt((WorldZ / VOXEL_SIZE) / StrateLayoutChunkWidthVoxels);
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkZ);
if (!FMath::IsFinite(WorldZ)) return false;
const int32 SlotIdx = FindSlotIndexForWorldZCentimetres(WorldZ);
if (SlotIdx < 0) return false;
const FStrateSlot& Slot = StrateLayout[SlotIdx];
// Voxel-space Z bounds → Unreal units. Ceiling = top chunk's upper edge.
OutTopZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels * VOXEL_SIZE;
OutBottomZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels * VOXEL_SIZE;
// Return the resolved physical bounds in Unreal centimeters.
OutTopZ = static_cast<float>(static_cast<double>(Slot.TopWorldMicrometres) / 10000.0);
OutBottomZ = static_cast<float>(static_cast<double>(Slot.BottomWorldMicrometres) / 10000.0);
return true;
}
@@ -6502,8 +6620,8 @@ FStrateDisturbanceParams UVoxelStrateManager::GetDisturbanceParamsForChunk(const
}
const FStrateSlot& Slot = StrateLayout[SlotIdx];
FStrateDisturbanceParams Result = Slot.Definition->Disturbances;
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Result.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
return Result;
}
@@ -6535,8 +6653,8 @@ float UVoxelStrateManager::GetWaterLevelWorldZForChunk(const FIntVector& ChunkCo
}
if (Rel <= 0.0f) return -FLT_MAX;
const float BottomZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
const float TopZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
const float BottomZ = Slot.LegacyBottomWorldZVoxels;
const float TopZ = Slot.LegacyTopWorldZVoxels;
return FMath::Lerp(BottomZ, TopZ, FMath::Clamp(Rel, 0.0f, 1.0f));
}
@@ -6580,8 +6698,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
FindComposerOverride(Slot.StrateIndex))
{
FStrateGenerationParams Result = Override->Params.TunnelNetworkParams;
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
// A candidate is a hard replacement of this slot. Do not blend its rolled vector with an
// authored neighbour at a boundary; this is also how the offline fixture measures it.
return ResolveFinalParams(Result);
@@ -6591,8 +6709,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
if (SeasonStrates.IsValidIndex(SlotIdx))
{
FStrateGenerationParams Result = SeasonStrates[SlotIdx].Params.TunnelNetworkParams;
Result.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
Result.StrateBottomWorldZ = (float)Slot.BottomChunkZ * StrateLayoutChunkWidthVoxels;
Result.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
Result.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
// Season vectors are already final, measured slot records. Vertical structural posts own
// their boundary; blending them with another selected recipe would describe neither one.
return ResolveFinalParams(Result);
@@ -6609,8 +6727,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
//
// TopChunkZ=0, CHUNK_SIZE=32: top of the strate = chunk 0's top edge = voxel Z=32
// BottomChunkZ=-3: bottom of the strate = chunk -3's bottom edge = voxel Z=-3*32 = -96
BaseParams.StrateTopWorldZ = (float)(Slot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
BaseParams.StrateBottomWorldZ = (float)(Slot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
BaseParams.StrateTopWorldZ = Slot.LegacyTopWorldZVoxels;
BaseParams.StrateBottomWorldZ = Slot.LegacyBottomWorldZVoxels;
// Core currently dispatches one generator field per resolved strate. Gradient and
// interleaved metadata therefore use the explicitly declared Hard fallback for every
@@ -6657,18 +6775,23 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
//---------------------------------------------------------------------
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
//---------------------------------------------------------------------
// DistFromBottom = how many chunks above the bottom edge of this strate.
// Legacy chunk callers project their sample to a layout interval; physical distance and
// transition width are both resolved in micrometres before projecting the blend fraction.
// When 0, we're right at the boundary. When == TransitionBlendChunks, we're at
// the outer edge of the transition zone.
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
const int64 DistFromBottomMicrometres = static_cast<int64>(ChunkCoord.Z)
* StrateLayoutChunkSizeMicrometres - Slot.BottomWorldMicrometres;
const int32 DistFromBottom = static_cast<int32>(VF_FloorDivide64(
DistFromBottomMicrometres, StrateLayoutChunkSizeMicrometres));
if (SlotIdx + 1 < StrateLayout.Num())
{
// The upper strate (this one) controls the transition type at its lower edge
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
const EVoxelStrateTransition TransType = Slot.ResolvedTransitionType;
// Per-definition blend distance.
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
Slot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
// Prepare the neighbor's params (only used for Gradient and Interleaved)
const FStrateSlot& BelowSlot = StrateLayout[SlotIdx + 1];
@@ -6692,8 +6815,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
if (EffectiveBlend > 0 && DistFromBottom < EffectiveBlend)
{
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
// Linear alpha: 0 at EffectiveBlend chunks away, 1 at the boundary
float Alpha = 1.0f - ((float)DistFromBottom / (float)EffectiveBlend);
@@ -6730,8 +6853,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
if (DistFromBottom < CheckRange)
{
FStrateGenerationParams BelowParams = BuildParamsFromDefinition(BelowSlot.Definition);
BelowParams.StrateTopWorldZ = (float)(BelowSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
BelowParams.StrateBottomWorldZ = (float)(BelowSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
BelowParams.StrateTopWorldZ = BelowSlot.LegacyTopWorldZVoxels;
BelowParams.StrateBottomWorldZ = BelowSlot.LegacyBottomWorldZVoxels;
// Sample 3D Perlin noise to warp the boundary position.
// XY frequency 0.15 gives medium-scale variation (~6-7 chunks per cycle).
@@ -6769,14 +6892,18 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
//---------------------------------------------------------------------
// Mirror logic: the ABOVE strate's TransitionType controls its lower edge,
// which is this strate's upper edge. So we read from StrateLayout[SlotIdx-1].
int32 DistFromTop = Slot.TopChunkZ - ChunkCoord.Z;
const int64 DistFromTopMicrometres = Slot.TopWorldMicrometres
- (static_cast<int64>(ChunkCoord.Z) + 1) * StrateLayoutChunkSizeMicrometres;
const int32 DistFromTop = static_cast<int32>(VF_FloorDivide64(
DistFromTopMicrometres, StrateLayoutChunkSizeMicrometres));
if (SlotIdx > 0)
{
// The strate ABOVE controls the transition at its lower edge (= our upper edge)
const FStrateSlot& AboveSlot = StrateLayout[SlotIdx - 1];
const EVoxelStrateTransition TransType = AboveSlot.Definition->TransitionType;
const int32 EffectiveBlend = AboveSlot.Definition->TransitionBlendChunks;
const EVoxelStrateTransition TransType = AboveSlot.ResolvedTransitionType;
const int32 EffectiveBlend = static_cast<int32>(VF_FloorDivide64(
AboveSlot.TransitionWidthMicrometres, StrateLayoutChunkSizeMicrometres));
switch (TransType)
{
@@ -6791,8 +6918,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
if (EffectiveBlend > 0 && DistFromTop < EffectiveBlend)
{
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
float Alpha = 1.0f - ((float)DistFromTop / (float)EffectiveBlend);
Alpha = FMath::Clamp(Alpha, 0.0f, 1.0f);
@@ -6813,8 +6940,8 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
if (DistFromTop < CheckRange)
{
FStrateGenerationParams AboveParams = BuildParamsFromDefinition(AboveSlot.Definition);
AboveParams.StrateTopWorldZ = (float)(AboveSlot.TopChunkZ + 1) * StrateLayoutChunkWidthVoxels;
AboveParams.StrateBottomWorldZ = (float)(AboveSlot.BottomChunkZ) * StrateLayoutChunkWidthVoxels;
AboveParams.StrateTopWorldZ = AboveSlot.LegacyTopWorldZVoxels;
AboveParams.StrateBottomWorldZ = AboveSlot.LegacyBottomWorldZVoxels;
// Same noise function but with a different seed offset to avoid
// symmetry between top and bottom boundaries of adjacent strates
+35 -4
View File
@@ -121,8 +121,8 @@ struct FVoxelPassage
* FStrateSlot — Runtime info for one strate in the layout.
*
* Created during Initialize() and stored in the StrateLayout array.
* Each slot knows its definition, its Z-range in chunk coordinates,
* and its index in the sequence.
* Each slot owns its resolved half-open physical Z interval. Chunk coordinates and legacy
* authoring values below are projections kept for existing callers and assets.
*/
USTRUCT()
struct FStrateSlot
@@ -136,6 +136,33 @@ struct FStrateSlot
// Strate index (0 = topmost, increases downward)
int32 StrateIndex = 0;
// Resolved physical bounds, in micrometres, with interval [bottom, top). These are computed
// once during Initialize from the configured metre chunk width and legacy asset dimensions.
int64 TopWorldMicrometres = 0;
int64 BottomWorldMicrometres = 0;
int64 HeightMicrometres = 0;
float LegacyTopWorldZVoxels = 0.0f;
float LegacyBottomWorldZVoxels = 0.0f;
float LegacyHeightVoxels = 0.0f;
// Immutable transition and passage inputs copied when this slot is resolved. The width is
// physical; chunk counts and raw owner-voxel values remain compatibility projections for
// legacy parameter and route adapters.
EVoxelStrateTransition ResolvedTransitionType = EVoxelStrateTransition::Gradient;
int32 ResolvedTransitionBlendChunks = 2;
int64 TransitionWidthMicrometres = 0;
FStratePassageConfig ResolvedPassageConfig;
int64 PassageMouthRadiusMicrometres = 0;
int64 PassageMidRadiusMicrometres = 0;
int64 PassageReachMinimumMicrometres = 0;
int64 PassageReachMaximumMicrometres = 0;
int64 PassageDistanceMinimumMicrometres = 0;
int64 PassageDistanceMaximumMicrometres = 0;
int64 PassageWanderMicrometres = 0;
int64 PassageVerticalWobbleMicrometres = 0;
int64 PassageSpiralRadiusMicrometres = 0;
int64 PassageCascadeLedgeMicrometres = 0;
// Z chunk range (inclusive). TopZ > BottomZ since Z decreases downward.
// Example: TopZ = 0, BottomZ = -3 means this strate spans chunks Z=0 to Z=-3
int32 TopChunkZ = 0;
@@ -248,6 +275,8 @@ public:
// This is independent of Core's 32^3 tile width.
float GetStrateLayoutChunkWidthVoxels() const { return StrateLayoutChunkWidthVoxels; }
uint32 GetStrateLayoutChunkSizeMicrometres() const { return StrateLayoutChunkSizeMicrometres; }
int64 GetResolvedInterStrateGapMicrometres() const
{ return ResolvedInterStrateGapMicrometres; }
// Called by the Core adapter through a const manager pointer; the flag is runtime routing
// state, not layout data, hence const + mutable.
void SetHardTransitionFallbackForAssembly(bool bEnabled) const
@@ -530,9 +559,10 @@ protected:
// Copied from VoxelSettings::OriginSpineRadius.
float OriginSpineRadius = 28.0f;
// Solid-bedrock gap between consecutive strates, in chunks. Copied from
// VoxelSettings::InterStrateGapChunks during Initialize().
// Compatibility gap projection, in layout chunks. Physical authority is the micrometre value
// resolved during Initialize below.
int32 InterStrateGapChunks = 0;
int64 ResolvedInterStrateGapMicrometres = 0;
// Resolved once from VoxelSettings::StrateLayoutChunkSizeMeters during Initialize().
float StrateLayoutChunkWidthVoxels = 32.0f;
@@ -554,6 +584,7 @@ protected:
// Find which slot a chunk Z coordinate falls into.
// Returns INDEX into StrateLayout, or -1 if not found.
int32 FindSlotIndexForChunkZ(int32 ChunkZ) const;
int32 FindSlotIndexForWorldZCentimetres(double WorldZCentimetres) const;
#if WITH_EDITOR
const FVoxelStrateComposerSlotOverride* FindComposerOverride(int32 StrateIndex) const;
@@ -0,0 +1,45 @@
# Linux 41 — resolved metre layout
Migration step 3 is implemented as a pure refactor. The verifier, manager and Core adapter changes are in the working tree; nothing was committed or pushed.
## Tool changes
Full-tier cost now builds a stamped baseline from `HEAD` by default; `--baseline <commit>` selects another commit. The build uses a temporary source tree. `--overall-timeout` defaults to 7,200 seconds for the tier. Each child process is killed at 1,800 seconds; builds remain capped at 12 jobs and the runner applies the requested CPU and address-space limits.
## Runtime authority
`UVoxelStrateManager::Initialize` resolves the configured layout width to integer micrometres before passage generation or scheduling. Slot height and gap are legacy chunk counts multiplied by that width. Each slot stores its physical `[bottom, top)` bounds and height, transition mode and width, copied passage settings and their micrometre dimensions. The effective seed is captured before passage generation. The Core adapter derives its interior bounds and L29 facing bands from those resolved values before work is scheduled.
The existing 8 m layout remains exact: the first upper edge is +8,000,000 µm; each height, gap or transition chunk is 8,000,000 µm. Chunk bounds are floor-based compatibility projections, including at negative Z. Season v2 still accepts only aligned, correctly stacked chunk bounds, then maps them to the same physical intervals. L29's bands keep their original faces: ceiling `[top-seal, top]`, content `[bottom+seal, top-seal]`, and bedrock `[bottom, bottom+seal]`; `seal` is the existing boundary-seal thickness converted from owner voxels to micrometres. Shared density and transition behavior are unchanged.
R6’s FI/SW split was already resolved: both plans and the snapshot use the same `LayoutChunkWidthSubvoxels` derived from the layout width. At the 25 cm Core voxel size, 8 m remains 32 Core voxels (8,192 subvoxels). Layout bounds do not use an owner/Core ratio. Ratios remain only in the legacy FloatingIslands parameter adapter. The legacy passage geometry adapter converts resolved passage micrometres back to owner voxels at its boundary.
Chunk indices remain in chunk-keyed compatibility APIs: `GetStrateForChunk`, generator and parameter queries, season-v2 validation, and legacy generator dispatch. They read the cached projection; world-Z lookup and Core snapshot bounds use physical intervals. L40’s bound Core field table remains the per-sample dispatch path and adds no manager lookup.
## Results
The final source stamp is `f35bc59c6172370e98dc83ec8ee5ba51dfab83a3faf075c26189fc83172f4ac8`; artifacts are in `Saved/Linux/l41`. The four 12-record FI/SW worker-1/worker-12 hash signatures match L40 exactly: FI `9820f4f38b189b3f7263810e7b3ff62aa502b21f476ffcfdbc7761a65cb07d58`; SW `a1e9268a4bbcc77d2a1ac54095df6851a70bab65c43ae50b332ed75132617834`. W1 and W12 also match each other. Layout IDs are FI `b7a7d9ecc48d8f26` and SW `8a52e84c26ed92d2`.
| Tier | Result |
|---|---|
| Fast | PASS, 163.1 s; 12/12 hashes for each recipe, worker parity and L40 identity pass. |
| Medium | PASS, 469.0 s; all checks pass, including transition and passage gates. |
| Full | PASS, 1,567.1 s; 107 runs and 539 checks, including the 30-cell determinism gate and cost pairs. |
Full cost compares against `HEAD` `efa9e89468cbab702c86fcbe451c8836167ae83a`. FI median is 17.627 s versus 17.665 s (−0.22%); SW is 1.819 s versus 1.821 s (−0.11%). The paired samples vary by up to +2.29% for FI and +1.27% for SW; neither median regressed.
Failed attempts: an initial verifier edit had an `IndentationError`; an overlapping fast run hit a mouth-profile failure, then the serial fast run passed. The first full attempt passed its gates but failed when the baseline build’s output name did not match the expected path. The corrected baseline name was rebuilt from `HEAD`; the final full run passed. Expected negative-control rejections passed their checks.
Unreal changes are limited to `VoxelStrateManager.h/.cpp` and `VoxelForgeCoreAdapter.cpp`; `CODEMAP.md` and `ARCHITECTURE.md` §8.51 are updated. The Unreal manager and adapter could not be compiled in this Linux workspace; validate them with the Windows Unreal build before integration.
## Orchestrator review (Claude)
Accepted after a re-check. **The source stamp did not cover the plugin sources that `core_sources.txt` compiles into
the Linux binaries** (for example `Source/VoxelForge/Private/VoxelStrateManager.cpp`). So L41's stamp equalled
L40's, and the tiers could not prove that they ran the edited manager. Fixed in `source_stamp.py`: the whole plugin
`Source/VoxelForge` tree and `Standalone/Compat` are now always stamped. Clean rebuild of both compilers (new stamp
`15cd0a95…`): fast PASS (owner hashes identical to L37), medium PASS, full PASS (1569 s; cost vs HEAD FI +0.71%,
SW +0.55%, within noise).
Owner decisions since R8 (2026-10-23): converted assets may change size slightly, so clean metre values beat
lossless legacy conversion. Cache sharing between players is optional (opt-in, with a data budget).
@@ -0,0 +1,69 @@
# R8 — metre authoring in Unreal assets
Read-only source and UE documentation review. No source, build, or test changes.
## 1. Reflected spatial-property inventory
Conversion notation: `n` is the old serialized number. Use double precision and integer micrometres at the adapter boundary. `owner voxel` = 0.125 m (`VOXEL_SIZE`); `Core voxel` = that world's saved `CoreVoxelSizeCm / 100` (default 0.25 m); legacy runtime chunk = 32 owner voxels = 4 m; vertical *layout* chunk = the captured old `StrateLayoutChunkSizeMeters` (8 m default in current manager code). Frequencies are cycles per old voxel: `f_new_per_m = f_old / oldVoxelMeters`. Inverse-frequency fallbacks must be converted with the same route.
| Reflected fields (source line) | Current unit and use | Convert to metres / losslessness |
|---|---|---|
| `UVoxelStrateDefinition::StrateHeightInChunks` ( `VoxelStrateDefinition.h:75` ), `TransitionBlendChunks` (`:103`), `UVoxelSettings::InterStrateGapChunks` (`VoxelSettings.h:420`) | Integer vertical layout chunks; manager assigns slot bounds and transition bands. | `heightM = count × captured old layoutChunkM`; same for gap and transition width. Exact and yields the same chunk indices only when old effective width is known and new metres remain an integer multiple of it. Current manager uses `StrateLayoutChunkSizeMeters` (`VoxelSettings.h:427`, default 8 m); the strate comment says 4 m (`VoxelStrateDefinition.h:71–73`). This must be resolved from each owner's pre-update world, not guessed. |
| `UVoxelSettings::StrateLayoutChunkSizeMeters` (`VoxelSettings.h:427`) | Already metres; physical vertical-layout chunk width, independent of Core tile/voxel width. | Keep as metres. It is the basis for converting height, gap and transition values, not the 4 m legacy streaming chunk. |
| `ViewDistanceXY/Up/Down` (`VoxelSettings.h:44,47,50`), `PawnCollisionRadiusChunks` (`:96`), `StrateViewMarginChunks` (`:111`), `RenderDistanceChunks` (`:183`), `DecorationRadiusChunks` / `DecorationNearRadiusChunks` (`:241,248`); `FStrateLandmark::SpacingChunks`, `StreamRadiusChunks`, `ExclusionRadiusChunks`, `SuppressRadiusChunks` (`VoxelStrateTypes.h:2298,2335,2344,2360`) | Legacy chunk counts controlling streaming extents, collision work, content grids and landmark placement. These routes define one chunk as 4 m (32 × 12.5 cm); landmark/content code multiplies by the runtime chunk width. | `m = n × 4`. Exact for the old grid. Keep `StrateLayoutChunkSizeMeters` separate; it is not this chunk. If these knobs remain integer counts internally, recover `round(m/4)` only for the old 4 m grid. |
| `WorldRadiusVoxels`, `EdgeSealThickness`, `OriginSpineRadius`, `MaxBrushRadius` (`VoxelSettings.h:222,226,440,465`); decoration spacing/far spacing/march step/column depth (`:263,271,277,289`); `FDecoSubCompanion::RadiusMinVox/RadiusMaxVox` (`VoxelStrateTypes.h:2127,2130`), `FDecoCompanion::RadiusMinVox/RadiusMaxVox` (`:2168,2171`); landmark orb radius/falloff/max-shadow distance (`:2383,2388,2393`) | Owner Runtime voxels at 12.5 cm. Used as radii, bounds, placement spacing/search steps, or light ranges. | `m = n × 0.125`. Exact physical conversion for the 12.5 cm grid. The spacing/step fields need metre-aware sampling, not just renamed UI labels. |
| `FStrateGenerationParams` (`VoxelStrateTypes.h`: `WormNetworkRange` 458; `RoomSpacing` 479; `MinRoomRadius` 496; `MaxRoomRadius` 504; `FloorReliefStrength` 552; `RoomMouthRiseBlendVoxels` 571; `OriginRoomRadius` 593; `TunnelMinRadius` 615; `TunnelMaxRadius` 622; `MaxTunnelLength` 637; `TunnelWarpStrength` 649; `TunnelFloorMaxLedgeHeight` 708; `SDFBlendRadius` 739; `CaveWarpStrength` 762; `SurfaceRoughness` 786; `DomainWarpStrength` 815; `FloorBias` 840; `BoundarySealThickness` 1076) | Owner-voxel distances/displacements used by cave morphology, warps, roughness, tunnel/room envelopes and seals. | `m = n × 0.125`. Exact for physical lengths. `TunnelFloorTerraceStepHeight` (`:702`) is serialized but explicitly ignored; keep it as deprecated compatibility data instead of converting a dead control. `TunnelEndpointZOffset` (`:678`) and `TunnelFloorGentleSlopeThreshold` (`:716`) are ratios/degrees, not lengths. |
| `FStrateGenerationParams` frequencies (`VoxelStrateTypes.h:418,560,771,793,822`) | `WormFrequency`, `FloorReliefFrequency`, `CaveWarpFrequency`, `RoughnessFrequency`, `DomainWarpFrequency`; inverse owner-voxel wavelength. | `cycles/m = f / 0.125` (multiply by 8). Exact change of units; resulting sampled output is byte-identical only if the evaluator uses the same operation/rounding order. |
| `FSlabGenerationParams` (`VoxelStrateTypes.h`: `FloorRoughness` 1259; `ColumnMinRadius` 1311; `ColumnMaxRadius` 1317; `ColumnSpacing` 1324; `CeilingRoughness` 1280; `BoundarySealThickness` 1334) and `FMazeGenerationParams` (`:CellSize` 1371; `CorridorRadius` 1377; `SurfaceRoughness` 1391; `BoundarySealThickness` 1395) | Owner-voxel geometry: surfaces, formations, columns, cells, corridors and seals. | `m = n × 0.125`, exact physically. Slab frequencies at `:1266,1288` use `cycles/m = f/0.125`. Relative floor/ceiling heights, density and probabilities remain dimensionless. |
| `FSurfaceGenerationParams` geometry (`VoxelStrateTypes.h`: `ElevationRange` 1430; `SurfaceRoughness` 1452; `HeightWarpStrength` 1460; `TerraceHeight` 1490; `LayerLineDepth` 1509; `LayerLineSpacing` 1513; `CliffSampleDist` 1537; `OverhangReach` 1554; `OverhangHeight` 1560; `BeachWidth` 1588; `CeilingRoughness` 1599; `CeilingUndulation` 1613; `CeilingRidgeStrength` 1625; `CeilingWarpStrength` 1635; `BoundarySealThickness` 1643) | Voxel lengths/displacements. **Current Core SurfaceWorld adapter consumes these raw in Core-voxel units** via `WorldAssemblyFixed`; the legacy owner path is 12.5 cm, while the current Core default is 25 cm. | To preserve the current Core recipe, `m = n × captured CoreVoxelSizeCm/100` (default `n×0.25`), not `n×0.125`. If preserving a legacy Runtime route instead, use that route's 0.125 m scale. This route distinction is essential. |
| `FSurfaceGenerationParams` frequencies (`VoxelStrateTypes.h:1434,1443,1447,1464,1470,1565,1604,1617,1629,1639`) | Continent, mountain, detail, height-warp, relief, overhang, ceiling roughness/undulation/ridge/warp frequencies. Current SurfaceWorld adapter treats them as cycles per Core voxel. | `cycles/m = f / captured CoreVoxelMeters` (×4 at 25 cm, ×8 at 12.5 cm). Use `fallbackM = fallbackOldVoxels × CoreVoxelMeters` when old frequency is zero. Exact unit conversion; see adapter caveat below. |
| `FVerticalShaftParams` (`VoxelStrateTypes.h`: `ShaftSpacing` 1672; radii 1680,1683; `ConnectorRadius` 1691; `LedgeSpacing` 1696; `LedgeDepth` 1700; `SurfaceRoughness` 1704; `BoundarySealThickness` 1708), `FFloatingIslandParams` (`:IslandSpacing` 1737; radii 1746,1749; roughness 1766; `SDFBlendRadius` 1771; seal 1775), `FStrateDisturbanceParams` (`:ChasmSpacing/Radius` 1808,1810; bridge spacing/radius 1817,1819; ridge spacing/height/thickness 1826,1828,1830; seal 1838) | Owner voxels; FI comments explicitly define owner scale and current adapter converts FI dimensions by owner/Core ratio. | `m = n×0.125`; exact. Disturbance and shaft spacing become metres too. Density, thickness ratio and jitter remain unitless. |
| `FStratePassageConfig` (`VoxelStrateTypes.h`: `MouthRadius` 1886; `MidRadius` 1889; `ReachMin/Max` 1895,1898; `DistanceMin/Max` 1904,1907; `Wander` 1913; `VerticalWobble` 1922; `SpiralRadius` 1927; `CascadeLedge` 1938) | Owner voxels; current Core adapter explicitly maps these through `OwnerVoxelsToCoreQ`. | `m=n×0.125`; exact. `Connections`, `Segments`, `CascadeSteps` are counts; `SpiralTurns` is turns, so keep those. The adapter's fixed 12-owner-voxel bore/wall reserves also need to become explicit 1.5 m / 0.5 m constants. |
| `FBiomeMapParams` (`VoxelBiomeTypes.h`: `CellSize` 37; `WarpStrength` 42; `BorderBlend` 51; frequencies 46,63,71) | Widths and warp displacement are voxel units; current biome recipe adapter packs them as Core voxels. Climate/warp frequencies are inverse Core-voxel units on that route. | Width/displacement: `m=n×CoreVoxelMeters`; frequency: `cycles/m=f/CoreVoxelMeters`. The legacy owner evaluator instead requires 0.125 m. `ReliefContrast` is unitless. |
| `UVoxelTerrainOpDefinition` (`VoxelTerrainOpDefinition.h`: `TerraceStepHeight` 91; `LayerLineSpacing` 112; `RibbingSpacing` 127; `OverhangDepth` 174; arch radii 195,201; column radii 218,224; pit radii/depth 241,247,253; chimney radii/height 270,276,282; dome radii 297,303; `PinchStrength/PinchLength` 328,334) | Legacy owner-voxel distances and SDF radii/lengths; `PinchStrength` is a physical narrowing width in morphology. | `m=n×0.125`. `ScallopFrequency` (`:159`) and `OverhangFrequency` (`:179`) become `f/0.125` cycles/m. `LayerLineDepth` (`:118`) and `RibbingDepth` (`:133`) are density amplitudes, not distances; leave them outside metre conversion. |
| `FPlacementProfile::SurfaceOffset`, `LocationOffset`, `CullDistance` (`VoxelStrateTypes.h:2068,2072,2097`); `UVoxelStrateDefinition::CeilingLayerZOffset/FloorLayerZOffset` (`VoxelStrateDefinition.h:316,329`); `AVoxelWorld::CoreVoxelSizeCm` (`VoxelWorld.h:304`) | Unreal centimetres. Placement vector is post-snap world offset; layer offsets are vertical offsets; Core voxel field is a world resolution (12.5–25 cm). | Scalar/vector centimetres: `/100` per component. Core resolution: `CoreVoxelSizeMeters=cm/100`. Exact at micrometre precision for current values. Do not bake this world-level value into a shared strate asset if different worlds use different Core sizes. |
| `EditorBrushCenter`, `EditorBrushRadius`, `BiomePreviewWorldSize`, `BiomePreviewCenter` (`VoxelWorld.h:862,866,902,906`) | Brush center is Unreal cm; brush radius and preview window size/center are legacy owner-voxel coordinates. | Center cm `/100`; other values/components `×0.125`. `BiomePreviewCenter` is a coordinate, not a size, but its units still change. |
| `UVoxelSeasonAsset` mirrors (`VoxelSeasonAsset.h:56,59`) and `VoxelSettings::WorldAssemblyManifestJson` (`VoxelSettings.h:392`) | Reflected mirror values are voxel-valued; the JSON is serialized manifest text, not a scalar property. Manifest schema has `HeightInChunks`, `InterStrateGapChunks`, `OriginSpineRadius`, `WorldRadiusVoxels` and per-strate `HeightInChunks` (`VoxelSeasonManifest.h:88–91,164`). | Migrate the source JSON with its own schema-version step, using the same route-aware formulas; refresh mirrors and content hash. Do not multiply opaque JSON text or convert mirrors independently. CSV/DataTable import sources also need a separate migration. |
`ClipRadius` (`VoxelSettings.h:153`) and `AVoxelWorld::CoreClipRadius` (`VoxelWorld.h:308`) are tile counts, `SkirtCells` (`VoxelSettings.h:193`) scales with LOD step, and `CoarseTileCells`/clip levels/region sizes are algorithmic counts. They have no single metre conversion that preserves current semantics across LODs; keep as counts or redesign the algorithm around a physical extent. Runtime-only `FVoxelModification` in `VoxelDiffLayer.h` is not reflected (`UPROPERTY`) and is not in this inventory.
**Losslessness:** 0.125/0.25 m voxels and 4/8 m chunks map exactly to integer micrometres. The Core recipe uses 1/256 Core-voxel fixed point; old values reproduce its old integer Q only when converted from the actual route and old world resolution. Arbitrary new metre values round to that Q grid, so they are not universally lossless. Use one `llround` at the adapter boundary and compare the resulting integer fields.
## 2. UE5 migration mechanism
Use a plugin `FGuid` custom version registered with a static `FCustomVersionRegistration`; migrate loaded properties in each owning UObject's `Serialize`. UE documents this GUID/version pattern and `FArchive::UsingCustomVersion` for object-level asset conversion ([asset/package versioning](https://dev.epicgames.com/documentation/unreal-engine/versioning-of-assets-and-packages-in-unreal-engine?lang=en-US), [`FCustomVersionRegistration`](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Core/FCustomVersionRegistration), [`UsingCustomVersion`](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/Core/FArchive/UsingCustomVersion), [`UObject::Serialize`](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/CoreUObject/UObject/Serialize)).
```cpp
// Registered once in the plugin module; increment Version when a later migration is added.
Ar.UsingCustomVersion(FVoxelForgeAssetVersion::GUID);
Super::Serialize(Ar);
if (Ar.IsLoading() && Ar.CustomVer(FVoxelForgeAssetVersion::GUID) < MetersV1)
{
HeightMeters = double(StrateHeightInChunks_DEPRECATED) * CapturedOldLayoutChunkMeters;
TransitionWidthMeters = double(TransitionBlendChunks_DEPRECATED) * CapturedOldLayoutChunkMeters;
// Convert each nested parameter by its recorded legacy route, then mark schema state.
}
```
Run this for settings, strate, biome, terrain-op, and world owners. A world-specific `CoreVoxelSizeCm` and old effective layout-chunk width must be available to the converter; do not infer either from current CDO defaults. Existing `UVoxelStrateDefinition::PostLoad` is useful for object cleanup, but `PostLoad` is not called for newly-created objects ([UE `PostLoad`](https://dev.epicgames.com/documentation/unreal-engine/API/Runtime/CoreUObject/UObject/UObject/PostLoad?application_version=5.5)). `PostEditChangeProperty` is for editor reactions/previews after a user edit, not load migration; use it only to refresh validation/UI when a new metre field changes ([property-change event API](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/CoreUObject/FPropertyChangedEvent)).
For one transition release, keep each old reflected property as a deprecated `_DEPRECATED` field with the old type and old CDO default, and add the new `...Meters` property with `meta=(Units="m")`. On rename, use exact `PropertyRedirects` from the old serialized name to its deprecated holding property, then copy/convert in `Serialize`; redirects remap names/values on load but do not change units ([Core Redirects](https://dev.epicgames.com/documentation/en-us/unreal-engine/core-redirects-in-unreal-engine)). Keep deprecated BP references compilable until graphs are updated. Epic notes metadata is editor-only, so runtime conversion must use the actual numeric field, not `Units` metadata ([metadata specifiers](https://dev.epicgames.com/documentation/unreal-engine/metadata-specifiers-in-unreal-engine)). Keep the old CDO defaults for this release: old assets may omit values equal to their old CDO default. Convert old-version assets even when the new field has its default; do not mistake that for already migrated data.
For `USTRUCT` rows in DataTables, add a version-aware `PostSerialize(const FArchive&)` via `TStructOpsTypeTraits::WithPostSerialize`, or run the same conversion in a one-time editor asset upgrader. Reimported CSV/JSON is external source data and will overwrite the table, so migrate that source too. Keep redirects and deprecated fields through a full resave/reference audit, then remove them in a later version.
## 3. Owner checklist
- Back up the Windows project/source control state. Before updating, record each world's old `CoreVoxelSizeCm`, effective layout-chunk metres, settings, seed and exported `ResolvedLayoutSnapshotV3` (strata bounds, gaps, transition widths, passage Q values and hash). This resolves the code's 4 m comment / current 8 m default conflict.
- Update the plugin, open and compile affected Blueprints, then load/save every referenced Settings, Strate, Biome, TerrainOp, Season, world/level and DataTable asset. Check conversion logs and save every dirty package. An asset never loaded and saved still has its old on-disk values; cooking can consume an in-memory conversion but is not a source-asset resave.
- Replace old Blueprint property nodes and fix deprecation/redirect warnings. Convert CSV/JSON sources before reimport; verify Season manifest schema and content hash refreshed.
- Re-export the same worlds with the same seed. Compare integer micrometre bounds, transition fields, passage fixed-point values, recipe Q/wavelength fields and `ResolvedLayoutSnapshotV3` stable identity against the pre-update export; inspect a clean cook/reopen for missing assets and redirect warnings.
## 4. Core adapter mapping
- `AttachValidatedWorldAssembly` currently resolves layout chunk micrometres once (`VoxelForgeCoreAdapter.cpp:818–822`); snapshot interiors and transition width use chunk indices × that width (`:1098–1108`). Feed it metre-valued bounds/gap/transition as integer micrometres. For converted legacy assets, convert back to the same chunk indices and preserve the same integer fields/hash. Reject or extend the Core snapshot schema for heights that are not integral layout chunks; the current manager/resolved layout is chunk-indexed.
- Replace `WorldAssemblyFixed(value)` (`:67–71`, currently `round(value×256)` voxels) with `MetersToCoreQ(m) = round(m×1,000,000×256/CoreVoxelMicrometres)`. Replace `WorldAssemblyWavelength(f, fallbackVoxels)` (`:78–83`) with a metre-frequency form: `MetersToCoreQ(f>0 ? 1/f_cyclesPerMeter : fallbackMeters)`. For old SurfaceWorld and biome recipes, `n Core voxels → n×CoreVoxelMeters`, so current Q remains `n×256`; old frequency `f` becomes `f/CoreVoxelMeters`, preserving its wavelength Q.
- For Floating Islands and passages, current code uses the 12.5 cm owner/Core ratio (`:492–524`, `:1061–1085`). Convert old owner values once to `n×0.125m`, then call `MetersToCoreQ`; at 25 cm Core this yields the same `n×128` Q as the existing ratio. Do not multiply by the ratio a second time. Convert hard-coded 12/4 owner-voxel bore/reserve constants to 1.5/0.5 m.
- Set resolved `voxelSizeMicrometres` from `round(voxelSizeMeters×1e6)` (`:1098`). Current runtime still has one actor-level Core sampling size and writes that same size for every strate; until Core sampling supports mixed per-strate resolutions, validate that every resolved strate value equals the world's `CoreVoxelSizeCm/100` (normally 0.25 m) rather than publishing conflicting metadata.
- Exact parity means the converted adapter emits the same old integer Q, micrometre intervals and canonical V3 layout identity for each old asset. Keep the old schema path/rounding for converted assets until that comparison passes.
**Owner question:** Can you capture each world's pre-update layout export, including its effective layout chunk width and `CoreVoxelSizeCm` (especially any world not at 25 cm)?
@@ -0,0 +1,72 @@
# Unreal asset migration notes — resolved metre layout
Read-only schema review for L41. No Unreal source or assets changed; no build or tests run.
## Existing authored data
- `UVoxelStrateDefinition` is a `UPrimaryDataAsset`. Its persisted `StrateHeightInChunks` and
`TransitionBlendChunks` drive the vertical slot and lower-boundary transition. Their comments
still describe 4 m chunks, while `UVoxelSettings::StrateLayoutChunkSizeMeters` now defaults to
8 m. Keep the numeric fields as legacy authoring inputs and resolve them once using the layout
width selected for that world. Do not reinterpret the stored integers as metres or infer their
scale from the stale tooltip.
- `UVoxelSettings::InterStrateGapChunks` is another legacy chunk input. Per-strate
`FStratePassageConfig` is persisted with radii, reaches, placement distances, wander, wobble,
spiral radius and cascade ledge expressed in legacy voxels; style, connection count, and segment
counts are dimensionless/discrete. Retain the exact old effective voxel scale for each legacy
asset route, convert dimensional values once, then store the resolved physical passage request
and derived seed before any jobs are scheduled.
- L40/R6 confirms FI and SW already receive the same `LayoutChunkWidthSubvoxels` for layout
planning. Never apply an owner/Core ratio to layout bounds a second time. Keep that ratio only
in the legacy FI parameter adapter; convert passage and layout inputs at their own established
unit boundary.
## Season assets need a separate compatibility route
`UVoxelSeasonAsset` embeds the reviewed JSON plus copied schema/season/seed/hash metadata. Current
manifest schema v2 stores `height_in_chunks`, global `inter_strate_gap_chunks`, per-slot seed and
`top_world_z`/`bottom_world_z`; the composer derives those bounds with a fixed `* 32` convention.
The runtime validates those stored bounds against the live layout chunk width. V2 does not carry a
resolved transition width/band or passage config for generated slots; authored slots recover
content and passage settings through `source_definition_path`. The asset loader rejects a schema
version it does not recognize and verifies both hash copies.
For migration, keep a v2 reader/converter that interprets v2 under its original coordinate
convention, verifies its existing content hash first, then builds the new immutable metre snapshot.
Do not rewrite the embedded v2 JSON or silently add default passages: either action changes
identity or geometry. If a season is explicitly re-authored/exported in a new schema, include
resolved metre bounds, gap, interface transition/band records, per-strate seed and passage settings;
bump the manifest schema and generate a new canonical content hash. Record the source schema and
conversion policy in diagnostics so stale or ambiguous assets fail closed rather than guessing.
## Resolved snapshot and edge rules
The game-thread director should publish integer micrometre records before queueing work: half-open
strate bounds `[bottom, top)`, height, inter-strate gap, transition width, the exact L29 upper/lower
band bounds and mode, world/strate/passages seeds, and fully resolved passage descriptors. Copy
the L29 band endpoints and cap rules exactly; do not recompute a second approximation from chunk
indices in the adapter. Derive legacy chunk fields only as projections for streaming, old Blueprint
queries and logging.
Use one conversion and edge policy in manager and adapters. For an aligned half-open interval the
first chunk is `floor(bottom / chunkWidth)` and the inclusive last chunk is `ceil(top / chunkWidth)
- 1` (equivalently `top / chunkWidth - 1` when aligned). Use mathematical floor / Euclidean
division for negative Z, not C++ truncation toward zero. Quantize authored metres to integer
micrometres once with a documented tie rule; avoid converting through float chunk positions and
rounding again. Test exact lower/upper edges, one micrometre either side, negative boundaries,
zero gap, and adjacent bands in both adapter and manager.
## Version and identity risks
- Keep authored-schema version, resolved-layout schema, recipe/evaluator version, and source
content hash distinct. A derivation-only schema change may alter layout diagnostics; it must not
move the owner density/mesh hashes in this pure-refactor step.
- For old chunk/voxel assets, preserve physical dimensions with a versioned compatibility
conversion. Future metre-authored properties should be explicit and versioned; do not overwrite
old fields or treat their raw values as metres. Keep original asset paths/values available for
rollback and audit.
- `FStratePassageConfig` currently has no persisted passage seed, and season v2 lacks generated-slot
passage descriptors. Derive seeds with the existing stable world-seed/slot/connection salts and
record the result in the snapshot; do not base it on UObject pointer or load order.
- UHT/UBT validation is still required for any reflected schema/property migration. The Linux Core
checks cannot validate UObject serialization, cooked season references, or Blueprint compatibility.
+3 -1
View File
@@ -55,8 +55,10 @@ def first_stamp(text: str) -> str | None:
def bounded_run(command: list[str], *, timeout: int = 1800,
cwd: Path = ROOT, env: dict[str, str] | None = None,
stdout: Any = subprocess.PIPE) -> subprocess.CompletedProcess[str] | None:
timeout = min(1800, max(1, int(timeout)))
try:
return subprocess.run([*LIMIT, "timeout", f"{timeout}s", *map(str, command)],
return subprocess.run([*LIMIT, "timeout", "--signal=KILL", f"{timeout}s",
*map(str, command)],
cwd=cwd, env=env, stdout=stdout, stderr=subprocess.STDOUT,
text=True, encoding="utf-8", errors="replace", timeout=timeout,
check=False)
+11 -3
View File
@@ -36,10 +36,18 @@ def manifest_entries(root: Path, include_unreal: bool = False) -> list[tuple[str
if helper.is_file():
files.add(helper)
# Plugin sources listed in core_sources.txt are compiled into the Linux binaries, and their
# headers are included by Core, so the whole plugin Source tree is always part of the stamp.
plugin_source = root / "Source" / "VoxelForge"
for suffix in ("*.h", "*.hpp", "*.cpp", "*.inl"):
files.update(plugin_source.rglob(suffix))
compat = standalone / "Compat"
for suffix in ("*.h", "*.hpp", "*.cpp"):
files.update(compat.rglob(suffix))
if include_unreal:
unreal = source / "VoxelForge"
for suffix in ("*.h", "*.hpp", "*.cpp", "*.cs"):
files.update(unreal.rglob(suffix))
for suffix in ("*.cs",):
files.update(plugin_source.rglob(suffix))
entries = [(path.relative_to(root).as_posix(), path.read_bytes())
for path in files if path.is_file()]
+59 -34
View File
@@ -10,6 +10,7 @@ import os
from pathlib import Path
import re
import shutil
import signal
import statistics
import struct
import subprocess
@@ -22,12 +23,16 @@ ROOT = Path(__file__).resolve().parents[3]
VERIFY_DIR = ROOT / "Standalone/Tools/verify"
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
OWNER = ROOT / "Standalone/data/owner_world.json"
OUT_DEFAULT = ROOT / "Saved/Linux/l39"
OUT_DEFAULT = ROOT / "Saved/Linux/l41"
PARSER = argparse.ArgumentParser(description=__doc__)
PARSER.add_argument("tier", choices=("fast", "medium", "full"))
PARSER.add_argument("--out", type=Path, default=OUT_DEFAULT,
help="artifact directory (default: Saved/Linux/l39)")
help="artifact directory (default: Saved/Linux/l41)")
PARSER.add_argument("--baseline", default="HEAD",
help="commit used for the full-tier cost baseline (default: HEAD)")
PARSER.add_argument("--overall-timeout", type=int, default=7200,
help="maximum tier wall time in seconds (default: 7200; each child is capped at 1800)")
ARGS = PARSER.parse_args()
OUT = ARGS.out.resolve()
os.environ["VOXELFORGE_VERIFY_OUT"] = str(OUT)
@@ -40,9 +45,9 @@ from runner import VerificationFailure, VerificationRunner, bounded_run
from source_stamp import source_stamp
CLANG = OUT / "bin/VoxelForgeCoreHeadless-l39-clang"
GCC = OUT / "bin/VoxelForgeCoreHeadless-l39-gcc"
RENDERER = OUT / "bin/VoxelForgeOffscreenCapture-l39-clang"
CLANG = OUT / "bin/VoxelForgeCoreHeadless-l41-clang"
GCC = OUT / "bin/VoxelForgeCoreHeadless-l41-gcc"
RENDERER = OUT / "bin/VoxelForgeOffscreenCapture-l41-clang"
HASH_RE = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
RECIPE_RE = re.compile(
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
@@ -481,19 +486,26 @@ def run_30_cell_determinism(runner: VerificationRunner) -> None:
runner.require("determinism-30-cell-count", count == 30, f"cells={count}/30")
def prepare_l36_cost_baseline(runner: VerificationRunner) -> tuple[Path, str]:
temp = Path("/tmp/voxelforge-l36-stamped")
def prepare_cost_baseline(runner: VerificationRunner) -> tuple[Path, str]:
resolved = bounded_run(["git", "rev-parse", "--verify", f"{ARGS.baseline}^{{commit}}"], timeout=120)
if resolved is None or resolved.returncode != 0:
raise VerificationFailure(f"could not resolve cost baseline commit {ARGS.baseline!r}")
baseline_commit = resolved.stdout.strip()
if not re.fullmatch(r"[0-9a-f]{40,64}", baseline_commit):
raise VerificationFailure(f"git returned an invalid baseline commit: {baseline_commit!r}")
temp = Path(f"/tmp/voxelforge-cost-baseline-{baseline_commit[:12]}")
if temp.exists():
shutil.rmtree(temp)
temp.mkdir(parents=True)
tarball = temp / "l36.tar"
tarball = temp / "baseline.tar"
archive = bounded_run(["git", "archive", "-o", str(tarball),
"8be1c4d", "Standalone"], timeout=120)
baseline_commit, "Standalone"], timeout=120)
if archive is None or archive.returncode != 0 or not tarball.is_file():
raise VerificationFailure("could not export historical L36 source commit 8be1c4d")
raise VerificationFailure(
f"could not export Standalone from cost baseline commit {baseline_commit}")
extract = bounded_run(["tar", "-xf", str(tarball), "-C", str(temp)], timeout=120)
if extract is None or extract.returncode != 0:
raise VerificationFailure("historical L36 source extraction failed")
raise VerificationFailure("cost baseline source extraction failed")
tarball.unlink(missing_ok=True)
base = temp / "Standalone"
@@ -508,55 +520,57 @@ def prepare_l36_cost_baseline(runner: VerificationRunner) -> tuple[Path, str]:
"#include \"SourceStamp.h\"\n#include <cstring>\n#include \"OwnerWorldLoader.h\"", 1)
anchor = re.search(r"int main\(int argc,\s*char\*\*\s*argv\)\s*\{", text)
if anchor is None:
raise VerificationFailure("cannot instrument historical L36 headless entry point")
raise VerificationFailure("cannot instrument cost-baseline headless entry point")
injected = ("\n VoxelForgePrintSourceStamp();\n"
" if(argc==2&&std::strcmp(argv[1],\"--print-source-stamp\")==0)return 0;")
text = text[:anchor.end()] + injected + text[anchor.end():]
legacy_main.write_text(text, encoding="utf-8")
baseline_stamp = source_stamp(temp)
binary = OUT / "historical-bin/VoxelForgeCoreHeadless-l36-stamped"
baseline_variant = f"baseline-{baseline_commit[:12]}-stamped"
binary = OUT / "historical-bin" / f"VoxelForgeCoreHeadless-{baseline_variant}"
env = os.environ.copy()
env["VOXELFORGE_BUILD_ROOT"] = str(temp / "build")
env["VOXELFORGE_BIN_DIR"] = str(binary.parent)
command = ["bash", str(base / "BuildLinux.sh"), "--compiler=clang++-19",
"--variant=l36-stamped", "--fp-contract=off", "--jobs=12"]
log = OUT / "cost-baseline-build-l36.log"
f"--variant={baseline_variant}", "--fp-contract=off", "--jobs=12"]
log = OUT / "cost-baseline-build.log"
log.parent.mkdir(parents=True, exist_ok=True)
started = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
proc = bounded_run(command, timeout=1800, env=env, stdout=stream)
elapsed = time.monotonic() - started
runner.require("cost-baseline-l36-build",
runner.require("cost-baseline-build",
proc is not None and proc.returncode == 0 and binary.is_file(),
f"source_stamp={baseline_stamp} elapsed_seconds={elapsed:.1f} log={log}")
f"baseline={baseline_commit} source_stamp={baseline_stamp} "
f"elapsed_seconds={elapsed:.1f} log={log}")
runner.binary_stamp_overrides[str(binary)] = baseline_stamp
return binary, baseline_stamp
def run_cost_pairs(runner: VerificationRunner) -> None:
baseline, baseline_stamp = prepare_l36_cost_baseline(runner)
baseline, baseline_stamp = prepare_cost_baseline(runner)
candidate = CLANG
orders = (("l36", "l39"), ("l39", "l36"), ("l36", "l39"),
("l39", "l36"), ("l36", "l39"))
orders = (("baseline", "candidate"), ("candidate", "baseline"),
("baseline", "candidate"), ("candidate", "baseline"),
("baseline", "candidate"))
by_recipe: dict[str, dict[str, list[float]]] = {
"floating-islands": {"l36": [], "l39": []},
"surface-world": {"l36": [], "l39": []}}
"floating-islands": {"baseline": [], "candidate": []},
"surface-world": {"baseline": [], "candidate": []}}
by_pair: dict[tuple[str, int], dict[str, float]] = {}
for recipe, style in (("floating-islands", 2), ("surface-world", 1)):
for pair, order in enumerate(orders, 1):
for build in order:
binary = baseline if build == "l36" else candidate
expected_style = None if build == "l36" else style
binary = baseline if build == "baseline" else candidate
name = f"cost-{recipe}-pair{pair}-{build}"
expected_stamp = baseline_stamp if build == "l36" else None
expected_stamp = baseline_stamp if build == "baseline" else None
row, output = runner.run_cpp(name, binary, OWNER, recipe, [],
expected_style=2 if build == "l39" else None,
expected_strate_styles={"0": 2, "1": 1} if build == "l39" else None,
expected_style=2 if build == "candidate" else None,
expected_strate_styles={"0": 2, "1": 1} if build == "candidate" else None,
layout_path=OUT / "layouts" / f"{name}.json",
expected_stamp=expected_stamp)
row["requested_style"] = style
row["expected_passage_style"] = 2 if build == "l39" else None
row["expected_passage_style"] = 2 if build == "candidate" else None
ready = re.findall(r"\bwhole_view_ready=([0-9.]+)s", output)
ok = (len(ready) == 1 and "hash=off" in output
and "cache_hits=0" in output
@@ -567,14 +581,14 @@ def run_cost_pairs(runner: VerificationRunner) -> None:
by_recipe[recipe][build].append(float(ready[0]))
by_pair.setdefault((recipe, pair), {})[build] = float(ready[0])
for recipe, values in by_recipe.items():
old = statistics.median(values["l36"])
new = statistics.median(values["l39"])
paired = [((by_pair[(recipe, pair)]["l39"] - by_pair[(recipe, pair)]["l36"])
/ by_pair[(recipe, pair)]["l36"] * 100.0)
old = statistics.median(values["baseline"])
new = statistics.median(values["candidate"])
paired = [((by_pair[(recipe, pair)]["candidate"] - by_pair[(recipe, pair)]["baseline"])
/ by_pair[(recipe, pair)]["baseline"] * 100.0)
for pair in range(1, 6)]
runner.require(f"cost-{recipe}-five-pairs",
len(values["l36"]) == len(values["l39"]) == 5,
f"median_l36_s={old:.3f} median_l39_s={new:.3f} "
len(values["baseline"]) == len(values["candidate"]) == 5,
f"median_baseline_s={old:.3f} median_candidate_s={new:.3f} "
f"median_change_pct={(new / old - 1.0) * 100.0:+.2f} "
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}")
@@ -598,9 +612,18 @@ def run_gcc_supplement(runner: VerificationRunner) -> None:
run_transitions_and_invariants(runner, (GCC,))
def _tier_deadline(_signum: int, _frame: object) -> None:
raise VerificationFailure(
f"{ARGS.tier} tier exceeded its {ARGS.overall_timeout}s overall timeout")
def main() -> int:
runner = VerificationRunner(ARGS.tier)
started = time.monotonic()
if ARGS.overall_timeout <= 0:
raise VerificationFailure("--overall-timeout must be a positive number of seconds")
signal.signal(signal.SIGALRM, _tier_deadline)
signal.setitimer(signal.ITIMER_REAL, ARGS.overall_timeout)
try:
if not OWNER.is_file():
raise VerificationFailure(f"canonical owner input is missing: {OWNER}")
@@ -634,6 +657,8 @@ def main() -> int:
runner.save_summary("FAIL", str(error))
print(f"[{ARGS.tier}] DONE FAIL elapsed={elapsed:.1f}s error={error}", flush=True)
return 1
finally:
signal.setitimer(signal.ITIMER_REAL, 0)
if __name__ == "__main__":