parity: Unreal and headless resolve the same four-strate plan (L70, v214)
- Export commandlet writes the live Core adapter's world_assembly + resolved layout pair; the headless owner path imports that pair (manifest/layout equal, layout_chunk_width_q validated). owner_world.json re-exported from the owner's staged assets on the Linux host. - Adapter SW route: Core clearance/radius floors, 0.5 m support search, outward-facing tie-break; passage mouth/mid radius floors match Core (36/24 owner voxels). - FI in Unreal now uses the legacy (compiled analytic) generator unless a plan is imported: the auto native-FI plan failed the passage audit on the owner plan (1843 blocked samples, 945 open-air violations). Owner decides whether native FI returns. - In-engine spawn traces per strate (3 each): SW GenerateTile 42.7 worker-s vs 20.1 headless for equal tile counts; LOD0 p95 45-169 ms in UE. - Fast tier green; all three passages pass on the shared plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
81e2452a3d
commit
bd68aee1ca
16 files changed
+939
-62
No files matched your search
@@ -255,7 +255,10 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
constexpr std::int64_t HalfHeightQ = PlayerHeightQ / 2;
|
||||
constexpr std::int64_t StepQ = 128;
|
||||
constexpr std::int64_t ProbeRadiusQ = 174;
|
||||
constexpr std::int64_t SupportDepthQ = 256;
|
||||
// The live owner surface recipe can sit slightly below the fixed-point route
|
||||
// heightfield used during candidate search; allow up to 0.5 m for the final
|
||||
// resolved-field support probe before rejecting an otherwise clear route.
|
||||
constexpr std::int64_t SupportDepthQ = 512;
|
||||
constexpr std::int64_t GradeQ16 = 17560;
|
||||
constexpr std::int64_t RouteRadiusQ = 4 * WorldAssemblySubvoxelsPerVoxel;
|
||||
struct FDirection { std::int32_t X; std::int32_t Y; };
|
||||
@@ -265,6 +268,7 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
struct FCandidate
|
||||
{
|
||||
std::int64_t Score = 0;
|
||||
std::int64_t OutwardProjection = 0;
|
||||
std::uint64_t EndpointProximityCost = 0;
|
||||
std::int32_t CenterX = 0;
|
||||
std::int32_t CenterY = 0;
|
||||
@@ -281,7 +285,7 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
Constraints.sweepSpacingSubvoxels = StepQ;
|
||||
Constraints.supportProbeRadiusSubvoxels = ProbeRadiusQ;
|
||||
Constraints.supportSearchDepthSubvoxels = SupportDepthQ;
|
||||
Constraints.minimumClearanceSubvoxels = 8;
|
||||
Constraints.minimumClearanceSubvoxels = 128;
|
||||
Constraints.minimumSupportProbes = 3;
|
||||
|
||||
const Heightfield2D BaseField(Recipe, Stratum);
|
||||
@@ -361,6 +365,8 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
{
|
||||
if (A.EndpointProximityCost != B.EndpointProximityCost)
|
||||
return A.EndpointProximityCost < B.EndpointProximityCost;
|
||||
if (A.OutwardProjection != B.OutwardProjection)
|
||||
return A.OutwardProjection > B.OutwardProjection;
|
||||
if (A.Score != B.Score) return A.Score < B.Score;
|
||||
if (A.CenterY != B.CenterY) return A.CenterY < B.CenterY;
|
||||
if (A.CenterX != B.CenterX) return A.CenterX < B.CenterX;
|
||||
@@ -498,6 +504,13 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe,
|
||||
if (Index == 32 && DepartureRadiusQ > 0)
|
||||
{
|
||||
Candidate.EndpointProximityCost += static_cast<std::uint64_t>(XQ * XQ + YQ * YQ);
|
||||
// Approach the surface portal from the world-facing side. The
|
||||
// resolved passage leaves the mouth along its outward tangent;
|
||||
// a route that approaches from beyond the mouth overlaps that
|
||||
// bore and turns its centerline into solid ground.
|
||||
const std::int64_t OutwardProjection = XQ * Directions[DirectionIndex].X
|
||||
+ YQ * Directions[DirectionIndex].Y;
|
||||
Candidate.OutwardProjection = OutwardProjection;
|
||||
}
|
||||
const auto Column = BaseField.point(WorldAssemblyFixed(static_cast<double>(X)),
|
||||
WorldAssemblyFixed(static_cast<double>(Y)), 0);
|
||||
@@ -1515,10 +1528,10 @@ bool AttachCoreGeometryOnlyLayout(const UVoxelStrateManager* StrateManager,
|
||||
Config.style = static_cast<std::uint32_t>(Passage.Style);
|
||||
Config.mouthRadiusSubvoxels = std::max(WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMouthRadiusMicrometres, CoreVoxelMicrometres),
|
||||
WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
|
||||
WorldAssemblyMicrometresToSubvoxels(36 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
|
||||
Config.midRadiusSubvoxels = std::max(WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMidRadiusMicrometres, CoreVoxelMicrometres),
|
||||
WorldAssemblyMicrometresToSubvoxels(12 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
|
||||
WorldAssemblyMicrometresToSubvoxels(24 * OwnerVoxelMicrometres, CoreVoxelMicrometres));
|
||||
Config.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageReachMinimumMicrometres, CoreVoxelMicrometres);
|
||||
Config.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageReachMaximumMicrometres, CoreVoxelMicrometres);
|
||||
Config.distanceMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(Slot.PassageDistanceMinimumMicrometres, CoreVoxelMicrometres);
|
||||
@@ -1635,7 +1648,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
Generated.plan.strates.push_back(std::move(Stratum));
|
||||
++BuiltSurfaceCount;
|
||||
}
|
||||
else if (Type == ECaveGeneratorType::FloatingIslands)
|
||||
// Native floating-island evaluation remains an explicit imported-plan opt-in.
|
||||
// The default owner path uses the legacy generator for this slot; emitting an
|
||||
// automatic FI plan here silently switches density dispatch and changes the
|
||||
// shared passage mouth before the owner has opted into that field.
|
||||
else if (Type == ECaveGeneratorType::FloatingIslands && bImportedOverride)
|
||||
{
|
||||
if (!BuildFloatingIslandStratumPlan(StrateManager, Slot, Generated,
|
||||
CoreVoxelSizeCm, LayoutChunkWidthSubvoxels, OutError))
|
||||
@@ -1708,10 +1725,11 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
}
|
||||
}
|
||||
if (Manifest->plan.strates.size() != static_cast<std::size_t>(SurfaceSlotCount)
|
||||
|| Manifest->floatingIslands.size() != static_cast<std::size_t>(FloatingIslandSlotCount))
|
||||
|| (!Manifest->floatingIslands.empty()
|
||||
&& Manifest->floatingIslands.size() != static_cast<std::size_t>(FloatingIslandSlotCount)))
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core world assembly has %d SurfaceWorld and %d floating-island plans for %d resolved SurfaceWorld and %d FloatingIslands strates."),
|
||||
TEXT("Core world assembly has %d SurfaceWorld and %d floating-island plans for %d resolved SurfaceWorld strates; an explicit FI plan must cover all %d FloatingIslands strates."),
|
||||
static_cast<int32>(Manifest->plan.strates.size()),
|
||||
static_cast<int32>(Manifest->floatingIslands.size()),
|
||||
SurfaceSlotCount, FloatingIslandSlotCount);
|
||||
@@ -1805,12 +1823,12 @@ bool AttachValidatedWorldAssembly(const UVoxelSettings* Settings,
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMouthRadiusMicrometres, ExpectedCoreVoxelMicrons),
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
36 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
PassageConfig.midRadiusSubvoxels = std::max(
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageMidRadiusMicrometres, ExpectedCoreVoxelMicrons),
|
||||
WorldAssemblyMicrometresToSubvoxels(
|
||||
12 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
24 * OwnerVoxelMicrons, ExpectedCoreVoxelMicrons));
|
||||
PassageConfig.reachMinimumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
Slot.PassageReachMinimumMicrometres, ExpectedCoreVoxelMicrons);
|
||||
PassageConfig.reachMaximumSubvoxels = WorldAssemblyMicrometresToSubvoxels(
|
||||
|
||||
@@ -13559,14 +13559,16 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
|
||||
const std::array<std::uint64_t, 4> Actual = StandaloneCoreAggregateHashes(Rows);
|
||||
const std::array<std::uint64_t, 4> Expected{
|
||||
0xdb2991c73215af4cull, 0x8236c5edd7fc8e4full,
|
||||
0x4c48fa75a543d58eull, 0xbea22e76b34a4dfcull};
|
||||
0xde345030348f2c5dull, 0xcbf0888004618b87ull,
|
||||
0x7a11e28e052d69a8ull, 0x52b88607e8dbcbfdull};
|
||||
const VoxelForgeCore::ShapeBuildStats Stats = CoreWorld.buildStats();
|
||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == 5642 && Stats.walkFailures == 0;
|
||||
if (Rows.size() != 2226 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||
constexpr std::uint64_t ExpectedWalkSamples = 21425;
|
||||
const bool bPassageWalkabilityPassed = Stats.walkSamples == ExpectedWalkSamples
|
||||
&& Stats.walkFailures == 0;
|
||||
if (Rows.size() != 2578 || Actual != Expected || !bPassageWalkabilityPassed)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected=5642/5642"),
|
||||
TEXT("Core adapter parity failed at workers=%llu: tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu expected_walk=%llu"),
|
||||
static_cast<unsigned long long>(WorkerLimit),
|
||||
static_cast<unsigned long long>(Rows.size()),
|
||||
static_cast<unsigned long long>(Actual[0]),
|
||||
@@ -13574,12 +13576,13 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
static_cast<unsigned long long>(Actual[2]),
|
||||
static_cast<unsigned long long>(Actual[3]),
|
||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
||||
static_cast<unsigned long long>(Stats.walkSamples));
|
||||
static_cast<unsigned long long>(Stats.walkSamples),
|
||||
static_cast<unsigned long long>(ExpectedWalkSamples));
|
||||
return false;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreAdapter] pass schema=1 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=5642/5642"),
|
||||
TEXT("[VoxelForgeCoreAdapter] pass schema=2 workers=%llu asset_clip_radius=%d asset_max_level=%d reference_clip_radius=3 reference_max_level=8 tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx walk=%llu/%llu"),
|
||||
static_cast<unsigned long long>(WorkerLimit),
|
||||
OwnerClipRadius,
|
||||
OwnerMaxLod,
|
||||
@@ -13587,7 +13590,9 @@ bool VerifyStandaloneCoreAdapter(const FString& Json, FString& OutError)
|
||||
static_cast<unsigned long long>(Actual[0]),
|
||||
static_cast<unsigned long long>(Actual[1]),
|
||||
static_cast<unsigned long long>(Actual[2]),
|
||||
static_cast<unsigned long long>(Actual[3]));
|
||||
static_cast<unsigned long long>(Actual[3]),
|
||||
static_cast<unsigned long long>(Stats.walkSamples - Stats.walkFailures),
|
||||
static_cast<unsigned long long>(ExpectedWalkSamples));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -13861,6 +13866,68 @@ bool RunStandaloneWorldExport(const FExploreArguments& Arguments, FString& OutEr
|
||||
Writer->WriteObjectEnd();
|
||||
Writer->Close();
|
||||
|
||||
// Export the exact Core manifest and resolved layout that the Unreal adapter just
|
||||
// generated from these live settings and slots. The standalone research tools consume
|
||||
// these snapshots instead of maintaining a second copy of the planning rules.
|
||||
FString RuntimeSnapshotJson;
|
||||
if (!VoxelForgeCoreAdapter::SerializeResolvedWorld(
|
||||
ExportSettings, Manager, 3, 8, StandaloneCoreVoxelSizeCm,
|
||||
RuntimeSnapshotJson, OutError))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FTCHARToUTF8 RuntimeSnapshotUtf8(*RuntimeSnapshotJson);
|
||||
std::string CoreSnapshotError;
|
||||
const std::optional<VoxelForgeCore::ResolvedWorldSnapshot> CoreSnapshot =
|
||||
VoxelForgeCore::ResolvedWorldSnapshot::FromJson(
|
||||
std::string_view(RuntimeSnapshotUtf8.Get(),
|
||||
static_cast<std::size_t>(RuntimeSnapshotUtf8.Length())), CoreSnapshotError);
|
||||
if (!CoreSnapshot || CoreSnapshot->worldAssembly() == nullptr
|
||||
|| CoreSnapshot->resolvedLayout() == nullptr)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Core adapter export did not produce a complete manifest and resolved layout: %s"),
|
||||
UTF8_TO_TCHAR(CoreSnapshotError.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string CoreAssemblyText = CoreSnapshot->worldAssembly()->ToJson();
|
||||
const std::string CoreLayoutText = CoreSnapshot->resolvedLayout()->ToJson();
|
||||
if (CoreAssemblyText.empty() || CoreLayoutText.empty())
|
||||
{
|
||||
OutError = TEXT("Core adapter export produced an empty canonical manifest or layout.");
|
||||
return false;
|
||||
}
|
||||
const FString CanonicalAssemblyJson = UTF8_TO_TCHAR(CoreAssemblyText.c_str());
|
||||
const FString CanonicalLayoutJson = UTF8_TO_TCHAR(CoreLayoutText.c_str());
|
||||
TSharedPtr<FJsonObject> ExportRoot;
|
||||
const TSharedRef<TJsonReader<>> ExportReader = TJsonReaderFactory<>::Create(Json);
|
||||
if (!FJsonSerializer::Deserialize(ExportReader, ExportRoot) || !ExportRoot.IsValid())
|
||||
{
|
||||
OutError = TEXT("Could not reopen the owner export to attach the Core plan snapshots.");
|
||||
return false;
|
||||
}
|
||||
TSharedPtr<FJsonObject> AssemblyObject;
|
||||
const TSharedRef<TJsonReader<>> AssemblyReader =
|
||||
TJsonReaderFactory<>::Create(CanonicalAssemblyJson);
|
||||
if (!FJsonSerializer::Deserialize(AssemblyReader, AssemblyObject) || !AssemblyObject.IsValid())
|
||||
{
|
||||
OutError = TEXT("Could not parse the Core adapter's canonical manifest for export.");
|
||||
return false;
|
||||
}
|
||||
ExportRoot->SetObjectField(TEXT("world_assembly"), AssemblyObject.ToSharedRef());
|
||||
ExportRoot->SetStringField(TEXT("core_world_assembly_json"), CanonicalAssemblyJson);
|
||||
ExportRoot->SetStringField(TEXT("resolved_layout_json"), CanonicalLayoutJson);
|
||||
FString ExportedJson;
|
||||
const TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> ExportWriter =
|
||||
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&ExportedJson);
|
||||
if (!FJsonSerializer::Serialize(ExportRoot.ToSharedRef(), ExportWriter))
|
||||
{
|
||||
OutError = TEXT("Could not serialize the owner export with its canonical Core plan.");
|
||||
return false;
|
||||
}
|
||||
Json = MoveTemp(ExportedJson);
|
||||
|
||||
const FString Path = FPaths::Combine(Arguments.OutDirectory, TEXT("owner_world.json"));
|
||||
if (!FFileHelper::SaveStringToFile(
|
||||
Json, *Path, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
|
||||
|
||||
Reference in new issue
Block a user