Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
323fbe3736 | ||
|
|
eb47d72258 | ||
|
|
95bf654fbc |
No files matched your search
@@ -0,0 +1,57 @@
|
|||||||
|
# AGENTS.md — VoxelForge (read automatically by Codex)
|
||||||
|
|
||||||
|
You are the implementation agent. The orchestrator (Claude) writes each round's brief, reviews your diff against the
|
||||||
|
code, and commits. The owner decides direction. Be warm and direct; credit what worked; say plainly what didn't.
|
||||||
|
|
||||||
|
Read first, as the task needs: `CURRENT-STATE.md` (what ships, reference numbers, **closed experiments: don't reopen
|
||||||
|
them without new evidence**), then `CODEMAP.md` and `ARCHITECTURE.md`. Round reports live in
|
||||||
|
`Standalone/Research/WorldAssemblerM3/Linux*/REPORT.md`.
|
||||||
|
|
||||||
|
## Standing rules (every round, unless the brief says otherwise)
|
||||||
|
|
||||||
|
- Run as user `jahni`. Write only inside this plugin, plus `/home/jahni/VFHost` and `/home/jahni/UnrealEngine` when
|
||||||
|
the brief asks for a UE build. No installs, no system changes.
|
||||||
|
- **No `git commit`, `push` or `stash`.** Leave changes in the working tree; the orchestrator reviews and commits.
|
||||||
|
- Headless processes and builds: `taskset -c 0-11 prlimit --as=6442450944` (12 P-core threads, 6 GiB). Never use all
|
||||||
|
cores.
|
||||||
|
- Unreal (UBT, the editor, commandlets): `taskset -c 0-11` only, **never `prlimit`** (CoreCLR fails). UE builds use
|
||||||
|
`-NoUBA -MaxParallelActions=4`, never while timing.
|
||||||
|
- Stop any single run that passes 30 minutes. Logs go in `Saved/Linux/l<round>/`.
|
||||||
|
- **Timing:** quiet machine only (check `uptime` first; load < ~1.5, nothing else building). L80 protocol: one warm-up
|
||||||
|
per binary, ≥5 interleaved runs, W8 and W12, all four owner layers. Every number states the window that produced it.
|
||||||
|
- **Same world for everyone:** a seed must give the same world on every machine and worker count (Clang and GCC,
|
||||||
|
W1/W8/W12). Byte-identity with older versions is NOT required. If the output changes: bump
|
||||||
|
`WorldAssemblyEvaluatorVersion`, add baselines, and show captures.
|
||||||
|
- Density sign: negative = solid, positive = air. Core voxels are 25 cm. Never mix metres and voxels without an
|
||||||
|
explicit conversion (v225 shipped a reach bug from exactly that).
|
||||||
|
- The fast tier (`Standalone/Tools/verify/verify.py fast`) must stay green. Don't loosen an audit to make a change pass.
|
||||||
|
|
||||||
|
## How to work
|
||||||
|
|
||||||
|
1. **Count before you build.** Measure where the time or the failure actually is (stack sampler, census, counters).
|
||||||
|
State which parts are measured and which are inferred.
|
||||||
|
2. **Suspect the measurement first.** An alarming or contradictory number is the instrument until a control rules it
|
||||||
|
out (A/A runs, an ablation that verifiably removes the work).
|
||||||
|
3. **No fix without a root cause.** Reproduce it, trace it to its source, fix the cause. A guard that hides the symptom
|
||||||
|
is not a fix.
|
||||||
|
4. **One change at a time,** each with its own check.
|
||||||
|
5. **Subtract before you add.** Prefer removing work to reorganising it. The smallest edit that solves the problem.
|
||||||
|
6. **Attack the premise.** When two fixes sharing one premise fail the same gate, write that premise in one sentence
|
||||||
|
and question it before trying a third. **After three failed fixes, stop:** it is probably the design, not the
|
||||||
|
hypothesis. Report it instead of trying fix #4.
|
||||||
|
7. **Tests must be able to fail.** Name what would make each check fail. Add a negative control when you add an audit.
|
||||||
|
8. **Refactors:** pin the behaviour (hashes), change the structure, prove nothing changed.
|
||||||
|
9. **Verify before you claim.** Don't report a check as passed unless it ran on the final source. Say which checks
|
||||||
|
didn't run and why.
|
||||||
|
10. Changes that speed things up go in the shared layer, so every generator benefits.
|
||||||
|
|
||||||
|
## Report
|
||||||
|
|
||||||
|
Write the round's `REPORT.md`, short and plain:
|
||||||
|
- the result first;
|
||||||
|
- what changed and why;
|
||||||
|
- the measurements, with their windows;
|
||||||
|
- the checks run, and those not run;
|
||||||
|
- remaining risks.
|
||||||
|
|
||||||
|
List any changed Unreal `Source/` files, because the owner compiles them on Windows.
|
||||||
@@ -262,6 +262,12 @@ namespace
|
|||||||
GVoxelForgeCoreRegionConcurrentAdmission,
|
GVoxelForgeCoreRegionConcurrentAdmission,
|
||||||
TEXT("Allow tiles independent of a pending Core region compile to proceed; 0 restores the global streaming stop."));
|
TEXT("Allow tiles independent of a pending Core region compile to proceed; 0 restores the global streaming stop."));
|
||||||
|
|
||||||
|
int32 GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles = 1;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingFastTravelWaitForAllCriticalTiles(
|
||||||
|
TEXT("voxel.StreamingFastTravelWaitForAllCriticalTiles"),
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles,
|
||||||
|
TEXT("Wait for the expanded critical tile set before sharing fast-travel admissions with near/far work; 0 keeps the walkable ring first and admits the normal shares."));
|
||||||
|
|
||||||
int32 GVoxelForgeStreamingResultBacklogCap = 256;
|
int32 GVoxelForgeStreamingResultBacklogCap = 256;
|
||||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingResultBacklogCap(
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingResultBacklogCap(
|
||||||
TEXT("voxel.StreamingResultBacklogCap"),
|
TEXT("voxel.StreamingResultBacklogCap"),
|
||||||
@@ -355,6 +361,11 @@ namespace
|
|||||||
TEXT("voxel.StreamingStartupApplyBoost"),
|
TEXT("voxel.StreamingStartupApplyBoost"),
|
||||||
GVoxelForgeStreamingStartupApplyBoost,
|
GVoxelForgeStreamingStartupApplyBoost,
|
||||||
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 24-mesh cap until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
TEXT("Bounded cold-view upload budget (default on): 1 allows a 14 ms / 24-mesh cap until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
||||||
|
float GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds = 0.0f;
|
||||||
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingFastTravelApplyBudgetMilliseconds(
|
||||||
|
TEXT("voxel.StreamingFastTravelApplyBudgetMilliseconds"),
|
||||||
|
GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds,
|
||||||
|
TEXT("Optional visible-result apply-time cap during speed-aware fast travel; 0 keeps the configured budget."));
|
||||||
constexpr int32 StreamingStartupMaxMeshApplies = 24;
|
constexpr int32 StreamingStartupMaxMeshApplies = 24;
|
||||||
constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f;
|
constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f;
|
||||||
constexpr double StreamingStartupApplyMaxSeconds = 30.0;
|
constexpr double StreamingStartupApplyMaxSeconds = 30.0;
|
||||||
@@ -2257,6 +2268,18 @@ void AVoxelWorld::BeginPlay()
|
|||||||
GVoxelForgeCoreRegionConcurrentAdmission =
|
GVoxelForgeCoreRegionConcurrentAdmission =
|
||||||
CommandLineCoreRegionConcurrentAdmission != 0 ? 1 : 0;
|
CommandLineCoreRegionConcurrentAdmission != 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingFastTravelWaitForAllCriticalTiles="),
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles =
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles != 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingFastTravelApplyBudgetMilliseconds="),
|
||||||
|
GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds))
|
||||||
|
{
|
||||||
|
GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds = FMath::Max(
|
||||||
|
0.0f, GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds);
|
||||||
|
}
|
||||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="),
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="),
|
||||||
CommandLineStreamingResultBacklogCap))
|
CommandLineStreamingResultBacklogCap))
|
||||||
{
|
{
|
||||||
@@ -2750,6 +2773,8 @@ void AVoxelWorld::BeginPlay()
|
|||||||
"\"collision_gate\":%d,"
|
"\"collision_gate\":%d,"
|
||||||
"\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d,"
|
"\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d,"
|
||||||
"\"streaming_far_apply_supplement\":%d,"
|
"\"streaming_far_apply_supplement\":%d,"
|
||||||
|
"\"streaming_fast_travel_wait_for_all_critical_tiles\":%d,"
|
||||||
|
"\"streaming_fast_travel_apply_budget_ms\":%.3f,"
|
||||||
"\"streaming_background_worker_reserve\":%d,\"core_parallel_for_background_priority\":%d,"
|
"\"streaming_background_worker_reserve\":%d,\"core_parallel_for_background_priority\":%d,"
|
||||||
"\"streaming_tile_background_low\":%d,\"streaming_tile_background_normal\":%d,"
|
"\"streaming_tile_background_low\":%d,\"streaming_tile_background_normal\":%d,"
|
||||||
"\"core_column_cache_direct_mapped\":%d,"
|
"\"core_column_cache_direct_mapped\":%d,"
|
||||||
@@ -2772,6 +2797,8 @@ void AVoxelWorld::BeginPlay()
|
|||||||
GVoxelForgeCollisionGate,
|
GVoxelForgeCollisionGate,
|
||||||
GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost,
|
GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost,
|
||||||
GVoxelForgeStreamingFarApplySupplement,
|
GVoxelForgeStreamingFarApplySupplement,
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles,
|
||||||
|
GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds,
|
||||||
GVoxelForgeStreamingBackgroundWorkerReserve,
|
GVoxelForgeStreamingBackgroundWorkerReserve,
|
||||||
GVoxelForgeCoreParallelForBackgroundPriority,
|
GVoxelForgeCoreParallelForBackgroundPriority,
|
||||||
GVoxelForgeStreamingTileBackgroundLow,
|
GVoxelForgeStreamingTileBackgroundLow,
|
||||||
@@ -9889,9 +9916,16 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
const int32 MaxApplies = GVoxelForgeStep1MaxMeshApplies > 0
|
const int32 MaxApplies = GVoxelForgeStep1MaxMeshApplies > 0
|
||||||
? GVoxelForgeStep1MaxMeshApplies
|
? GVoxelForgeStep1MaxMeshApplies
|
||||||
: (bUseStartupApplyBoost ? StreamingStartupMaxMeshApplies : ConfiguredMaxApplies);
|
: (bUseStartupApplyBoost ? StreamingStartupMaxMeshApplies : ConfiguredMaxApplies);
|
||||||
const float ApplyBudgetMilliseconds = bUseStartupApplyBoost
|
float ApplyBudgetMilliseconds = bUseStartupApplyBoost
|
||||||
? StreamingStartupApplyBudgetMilliseconds
|
? StreamingStartupApplyBudgetMilliseconds
|
||||||
: (Settings ? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f) : 2.0f);
|
: (Settings ? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f) : 2.0f);
|
||||||
|
const bool bFastTravelApplyBudgetActive = bStreamingPlayerMoving && bSpeedAwareFastMode
|
||||||
|
&& GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds > 0.0f;
|
||||||
|
if (bFastTravelApplyBudgetActive)
|
||||||
|
{
|
||||||
|
ApplyBudgetMilliseconds = FMath::Min(
|
||||||
|
ApplyBudgetMilliseconds, GVoxelForgeStreamingFastTravelApplyBudgetMilliseconds);
|
||||||
|
}
|
||||||
const double ApplyBudgetSeconds =
|
const double ApplyBudgetSeconds =
|
||||||
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
||||||
int32 MeshesApplied = 0;
|
int32 MeshesApplied = 0;
|
||||||
@@ -13231,7 +13265,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
// Policy 0 reconstructs L77. Policy 3 enters that order only after sustained fast travel.
|
// Policy 0 reconstructs L77. Policy 3 enters that order only after sustained fast travel.
|
||||||
if (bPolicy3FastTravel)
|
if (bPolicy3FastTravel)
|
||||||
{
|
{
|
||||||
if (bWalkableRingReadyForReserve && AreCriticalTilesReady())
|
const bool bWaitForExpandedCriticalSet =
|
||||||
|
GVoxelForgeStreamingFastTravelWaitForAllCriticalTiles != 0;
|
||||||
|
if (bWalkableRingReadyForReserve
|
||||||
|
&& (!bWaitForExpandedCriticalSet || AreCriticalTilesReady()))
|
||||||
{
|
{
|
||||||
const int32 ShareAdmissionCap = FastTravelTotalCap;
|
const int32 ShareAdmissionCap = FastTravelTotalCap;
|
||||||
const int32 StartingAdmissions = GetGenerationAdmissionCount();
|
const int32 StartingAdmissions = GetGenerationAdmissionCount();
|
||||||
@@ -13451,7 +13488,24 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
int32 SettledByLevel[9] = {};
|
int32 SettledByLevel[9] = {};
|
||||||
int32 CoveredByLevel[9] = {};
|
int32 CoveredByLevel[9] = {};
|
||||||
int32 PendingByLevel[9] = {};
|
int32 PendingByLevel[9] = {};
|
||||||
|
int32 NearAheadDesired = 0;
|
||||||
|
int32 NearAheadResident = 0;
|
||||||
|
int32 NearBehindDesired = 0;
|
||||||
|
int32 NearBehindResident = 0;
|
||||||
|
int32 NearForwardConeDesired = 0;
|
||||||
|
int32 NearForwardConeResident = 0;
|
||||||
|
double NearForwardWeightedDesired = 0.0;
|
||||||
|
double NearForwardWeightedResident = 0.0;
|
||||||
int32 TraceMaxDesiredLevel = 0;
|
int32 TraceMaxDesiredLevel = 0;
|
||||||
|
const FVector LocalPawn = WorldToLocalCm(CenterPosition);
|
||||||
|
FVector TraceHeading = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading)
|
||||||
|
.GetSafeNormal();
|
||||||
|
if (TraceHeading.IsNearlyZero() && IsValid(PlayerPawn))
|
||||||
|
{
|
||||||
|
TraceHeading = GetActorTransform().InverseTransformVectorNoScale(
|
||||||
|
PlayerPawn->GetActorForwardVector()).GetSafeNormal();
|
||||||
|
}
|
||||||
|
constexpr double ForwardConeCosine = 0.573576436351046; // cos(55 degrees)
|
||||||
for (const FVoxelTileKey& Tile : DesiredSorted)
|
for (const FVoxelTileKey& Tile : DesiredSorted)
|
||||||
{
|
{
|
||||||
TraceMaxDesiredLevel = FMath::Max(TraceMaxDesiredLevel, Tile.Level);
|
TraceMaxDesiredLevel = FMath::Max(TraceMaxDesiredLevel, Tile.Level);
|
||||||
@@ -13460,10 +13514,43 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
{
|
{
|
||||||
if (Tile.Level < 0 || Tile.Level > 8) continue;
|
if (Tile.Level < 0 || Tile.Level > 8) continue;
|
||||||
++DesiredByLevel[Tile.Level];
|
++DesiredByLevel[Tile.Level];
|
||||||
if (LoadedTiles.Contains(Tile)) ++ResidentByLevel[Tile.Level];
|
const bool bResident = LoadedTiles.Contains(Tile);
|
||||||
|
if (bResident) ++ResidentByLevel[Tile.Level];
|
||||||
if (LoadedTiles.Contains(Tile) || CollisionNotRequiredTiles.Contains(Tile))
|
if (LoadedTiles.Contains(Tile) || CollisionNotRequiredTiles.Contains(Tile))
|
||||||
++SettledByLevel[Tile.Level];
|
++SettledByLevel[Tile.Level];
|
||||||
if (PendingTiles.Contains(Tile)) ++PendingByLevel[Tile.Level];
|
if (PendingTiles.Contains(Tile)) ++PendingByLevel[Tile.Level];
|
||||||
|
|
||||||
|
if (Tile.Level <= 2 && !TraceHeading.IsNearlyZero())
|
||||||
|
{
|
||||||
|
const double TileExtentCm = static_cast<double>(
|
||||||
|
VF_ClipLevelExtentVoxels(Tile.Level)) * GetActiveVoxelSizeCm();
|
||||||
|
const FVector TileCenter(
|
||||||
|
(static_cast<double>(Tile.Coord.X) + 0.5) * TileExtentCm,
|
||||||
|
(static_cast<double>(Tile.Coord.Y) + 0.5) * TileExtentCm,
|
||||||
|
(static_cast<double>(Tile.Coord.Z) + 0.5) * TileExtentCm);
|
||||||
|
const FVector Offset = TileCenter - LocalPawn;
|
||||||
|
const double Distance = Offset.Size();
|
||||||
|
const double Projection = FVector::DotProduct(Offset, TraceHeading);
|
||||||
|
const double DirectionCosine = Distance > 0.0 ? Projection / Distance : 0.0;
|
||||||
|
if (Projection > 0.0)
|
||||||
|
{
|
||||||
|
++NearAheadDesired;
|
||||||
|
if (bResident) ++NearAheadResident;
|
||||||
|
}
|
||||||
|
else if (Projection < 0.0)
|
||||||
|
{
|
||||||
|
++NearBehindDesired;
|
||||||
|
if (bResident) ++NearBehindResident;
|
||||||
|
}
|
||||||
|
if (DirectionCosine >= ForwardConeCosine)
|
||||||
|
{
|
||||||
|
++NearForwardConeDesired;
|
||||||
|
if (bResident) ++NearForwardConeResident;
|
||||||
|
}
|
||||||
|
const double Weight = FMath::Max(0.0, DirectionCosine);
|
||||||
|
NearForwardWeightedDesired += Weight;
|
||||||
|
if (bResident) NearForwardWeightedResident += Weight;
|
||||||
|
}
|
||||||
bool bCovered = LoadedTiles.Contains(Tile);
|
bool bCovered = LoadedTiles.Contains(Tile);
|
||||||
for (int32 AncestorLevel = Tile.Level + 1;
|
for (int32 AncestorLevel = Tile.Level + 1;
|
||||||
!bCovered && AncestorLevel <= TraceMaxDesiredLevel && AncestorLevel <= 8;
|
!bCovered && AncestorLevel <= TraceMaxDesiredLevel && AncestorLevel <= 8;
|
||||||
@@ -13503,12 +13590,17 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
SettledCounts += TEXT("]");
|
SettledCounts += TEXT("]");
|
||||||
CoveredCounts += TEXT("]");
|
CoveredCounts += TEXT("]");
|
||||||
PendingCounts += TEXT("]");
|
PendingCounts += TEXT("]");
|
||||||
const FVector LocalPawn = WorldToLocalCm(CenterPosition);
|
|
||||||
const double StreamingUpdateMilliseconds = (TraceNow - StreamingUpdateStartSeconds) * 1000.0;
|
const double StreamingUpdateMilliseconds = (TraceNow - StreamingUpdateStartSeconds) * 1000.0;
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_sample"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_sample"), FString::Printf(
|
||||||
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
|
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
|
||||||
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d,"
|
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d,"
|
||||||
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"settled_by_lod\":%s,"
|
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"settled_by_lod\":%s,"
|
||||||
|
"\"near_ahead_desired\":%d,\"near_ahead_resident\":%d,"
|
||||||
|
"\"near_behind_desired\":%d,\"near_behind_resident\":%d,"
|
||||||
|
"\"near_forward_cone_half_angle_deg\":55,"
|
||||||
|
"\"near_forward_cone_desired\":%d,\"near_forward_cone_resident\":%d,"
|
||||||
|
"\"near_forward_weighted_desired\":%s,"
|
||||||
|
"\"near_forward_weighted_resident\":%s,\"near_direction_valid\":%d,"
|
||||||
"\"pending_by_lod\":%s,"
|
"\"pending_by_lod\":%s,"
|
||||||
"\"covered_by_lod\":%s,"
|
"\"covered_by_lod\":%s,"
|
||||||
"\"walkable_ready_tiles\":%d,\"walkable_required_tiles\":9,"
|
"\"walkable_ready_tiles\":%d,\"walkable_required_tiles\":9,"
|
||||||
@@ -13522,7 +13614,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
*FString::SanitizeFloat(LocalPawn.Z),
|
*FString::SanitizeFloat(LocalPawn.Z),
|
||||||
*FString::SanitizeFloat(PlayerHeading.Size()),
|
*FString::SanitizeFloat(PlayerHeading.Size()),
|
||||||
bWalkableRingReadyForReserve ? 1 : 0,
|
bWalkableRingReadyForReserve ? 1 : 0,
|
||||||
*DesiredCounts, *ResidentCounts, *SettledCounts, *PendingCounts, *CoveredCounts,
|
*DesiredCounts, *ResidentCounts, *SettledCounts,
|
||||||
|
NearAheadDesired, NearAheadResident, NearBehindDesired, NearBehindResident,
|
||||||
|
NearForwardConeDesired, NearForwardConeResident,
|
||||||
|
*FString::SanitizeFloat(static_cast<float>(NearForwardWeightedDesired)),
|
||||||
|
*FString::SanitizeFloat(static_cast<float>(NearForwardWeightedResident)),
|
||||||
|
TraceHeading.IsNearlyZero() ? 0 : 1,
|
||||||
|
*PendingCounts, *CoveredCounts,
|
||||||
WalkableRingReadyCount,
|
WalkableRingReadyCount,
|
||||||
GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve,
|
GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve,
|
||||||
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# L133 — native floating islands
|
|
||||||
|
|
||||||
Stopped after the second route design. The native v9 plan binds the owner’s resolved mouths and passes raw route audit, strict layout (0 issues), 3/3 interface audits, shell (0 floor violations), and ripple (3/3). The candidate dispatches FI natively.
|
|
||||||
|
|
||||||
The composed FI walk still fails: 2 capsule collisions, 40 unsupported centers, 0 grade failures; mouth binding passes. The return lane is carved by `passage/0-1/0` near `(19.755, 9.605, -733.553) m` (4.25 m from its 4.5 m tube). At `passage/1-2/0`, the approach has collisions at `(14.402, -10.552, -732.556) m` and `(14.242, -10.712, -732.556) m`; the departure landing probe `(19.166, -14.650, -733.331) m` gets 0/5 support probes. The exact endpoints alone are not enough to place a route clear of both composed passage volumes and retain a supported landing.
|
|
||||||
|
|
||||||
No captures, timing matrix, compiler determinism matrix, baseline update, or UE host build were run after the required composed walk gate failed. The owner’s default import remains on its legacy FI fallback. Logs and candidate fixtures are under `Saved/Linux/l133/`.
|
|
||||||
@@ -1,511 +0,0 @@
|
|||||||
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
||||||
index cb2b2dd..5a94ab8 100644
|
|
||||||
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
||||||
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
|
||||||
@@ -496,8 +496,9 @@ namespace VoxelForgeCore
|
|
||||||
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
|
||||||
// Version 226 unifies the passage influence reach in metres and converts it to core
|
|
||||||
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
|
|
||||||
- // support width/6 when deriving that reach.
|
|
||||||
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227;
|
|
||||||
+ // support width/6 when deriving that reach. Version 228 permits the native FI route
|
|
||||||
+ // planner to bind its fixed endpoints to the resolved passage mouths.
|
|
||||||
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 228;
|
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
|
||||||
{
|
|
||||||
@@ -788,6 +789,15 @@ namespace VoxelForgeCore
|
|
||||||
std::uint64_t contentHash = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
+ // The two resolved inter-strate walk points owned by a FloatingIslands slot.
|
|
||||||
+ // The route planner treats these as fixed player-capsule centers and places
|
|
||||||
+ // the island chain between them.
|
|
||||||
+ struct FloatingIslandRouteEndpointsV1
|
|
||||||
+ {
|
|
||||||
+ std::array<std::int64_t, 3> arrivalSubvoxels{};
|
|
||||||
+ std::array<std::int64_t, 3> departureSubvoxels{};
|
|
||||||
+ };
|
|
||||||
+
|
|
||||||
struct WorldAssemblyFeatureGroupBoundsV1
|
|
||||||
{
|
|
||||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
|
||||||
@@ -1377,6 +1387,9 @@ namespace VoxelForgeCore
|
|
||||||
// Planner output is ordinary reviewed data; clients can load it without re-running the planner.
|
|
||||||
VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
||||||
const FloatingIslandRecipeV1& recipe, std::string& error);
|
|
||||||
+ VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
||||||
+ const FloatingIslandRecipeV1& recipe,
|
|
||||||
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error);
|
|
||||||
VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(
|
|
||||||
const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256,
|
|
||||||
std::uint64_t seed = 0x49534c414e444231ull);
|
|
||||||
diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h
|
|
||||||
index 33d97d6..42f855a 100644
|
|
||||||
--- a/Standalone/Core/src/FloatingIslandsInternal.h
|
|
||||||
+++ b/Standalone/Core/src/FloatingIslandsInternal.h
|
|
||||||
@@ -285,7 +285,7 @@ namespace floating_island_detail
|
|
||||||
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
|
||||||
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
|
||||||
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
|
||||||
- if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
|
|
||||||
+ if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 9
|
|
||||||
|| (recipe.geometryVersion == 1
|
|
||||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
|
||||||
|| recipe.unitsPerVoxel != Q
|
|
||||||
@@ -1473,7 +1473,8 @@ namespace floating_island_detail
|
|
||||||
static_cast<double>(anchorIsland->centerSubvoxels[0]),
|
|
||||||
static_cast<double>(anchorIsland->centerSubvoxels[1]));
|
|
||||||
if(anchorDistance>recipe.anchorPlacementRadiusSubvoxels
|
|
||||||
- ||recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels
|
|
||||||
+ ||(recipe.geometryVersion<8
|
|
||||||
+ &&recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels)
|
|
||||||
||recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
||||||
>=anchorIsland->radiusSubvoxels)
|
|
||||||
{error="floating-island anchor island or its mouth leaves the passage radius or rock envelope";return false;}
|
|
||||||
@@ -1607,7 +1608,7 @@ namespace floating_island_detail
|
|
||||||
||to.ordinal==recipe.anchorIslandOrdinal);
|
|
||||||
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
|
|
||||||
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
|
|
||||||
- const double routeDeckLift=recipe.geometryVersion>=2
|
|
||||||
+ const double routeDeckLift=recipe.geometryVersion>=9?0.0:recipe.geometryVersion>=2
|
|
||||||
?(WorldAssemblyEvaluatorVersion>=178
|
|
||||||
?(westernSupportConnector
|
|
||||||
?(WorldAssemblyEvaluatorVersion>=181?-1.0
|
|
||||||
@@ -2264,6 +2265,335 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
||||||
return manifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
+std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
|
||||||
+ const FloatingIslandRecipeV1& inputRecipe,
|
|
||||||
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error)
|
|
||||||
+{
|
|
||||||
+ FloatingIslandManifestV1 manifest;
|
|
||||||
+ manifest.recipe = inputRecipe;
|
|
||||||
+ auto& recipe = manifest.recipe;
|
|
||||||
+ recipe.geometryVersion = 9;
|
|
||||||
+ recipe.primaryIslandCount = 9;
|
|
||||||
+ recipe.anchorIslandOrdinal = recipe.primaryIslandCount - 1;
|
|
||||||
+ constexpr std::int64_t step = 192 * floating_island_detail::Q;
|
|
||||||
+ recipe.spacingSubvoxels = step;
|
|
||||||
+ if (recipe.routeConstraints.version == 0)
|
|
||||||
+ {
|
|
||||||
+ recipe.routeConstraints.version = 1;
|
|
||||||
+ recipe.routeConstraints.capsuleRadiusSubvoxels = 348;
|
|
||||||
+ recipe.routeConstraints.capsuleHeightSubvoxels = 1802;
|
|
||||||
+ recipe.routeConstraints.maximumGradeQ16 = 17560;
|
|
||||||
+ recipe.routeConstraints.sweepSpacingSubvoxels = 128;
|
|
||||||
+ recipe.routeConstraints.supportProbeRadiusSubvoxels = 230;
|
|
||||||
+ recipe.routeConstraints.supportSearchDepthSubvoxels = 512;
|
|
||||||
+ recipe.routeConstraints.minimumClearanceSubvoxels = 128;
|
|
||||||
+ recipe.routeConstraints.minimumSupportProbes = 3;
|
|
||||||
+ }
|
|
||||||
+ for (const auto& value : endpoints.arrivalSubvoxels)
|
|
||||||
+ if (!floating_island_detail::ValidScalar(value))
|
|
||||||
+ { error = "resolved floating-island arrival mouth is outside the fixed-point range"; return std::nullopt; }
|
|
||||||
+ for (const auto& value : endpoints.departureSubvoxels)
|
|
||||||
+ if (!floating_island_detail::ValidScalar(value))
|
|
||||||
+ { error = "resolved floating-island departure mouth is outside the fixed-point range"; return std::nullopt; }
|
|
||||||
+
|
|
||||||
+ const double qPerMetre = static_cast<double>(floating_island_detail::Q)
|
|
||||||
+ * 1000000.0 / recipe.coreVoxelSizeMicrons;
|
|
||||||
+ const double dx = static_cast<double>(endpoints.departureSubvoxels[0]
|
|
||||||
+ - endpoints.arrivalSubvoxels[0]);
|
|
||||||
+ const double dy = static_cast<double>(endpoints.departureSubvoxels[1]
|
|
||||||
+ - endpoints.arrivalSubvoxels[1]);
|
|
||||||
+ const double endpointDistance = std::hypot(dx, dy);
|
|
||||||
+ const std::int64_t maximumMouthOffset = 64 * floating_island_detail::Q
|
|
||||||
+ - recipe.routeConstraints.capsuleRadiusSubvoxels - floating_island_detail::Q;
|
|
||||||
+ const std::int64_t desiredMouthOffset = std::max(recipe.anchorMouthOffsetSubvoxels,
|
|
||||||
+ static_cast<std::int64_t>(std::llround(qPerMetre * 6.6)));
|
|
||||||
+ recipe.anchorMouthOffsetSubvoxels = std::min({desiredMouthOffset,
|
|
||||||
+ maximumMouthOffset, static_cast<std::int64_t>(std::llround(endpointDistance))});
|
|
||||||
+ if (recipe.anchorMouthOffsetSubvoxels <= 0)
|
|
||||||
+ { error = "resolved island mouths cannot provide a bounded departure approach"; return std::nullopt; }
|
|
||||||
+ recipe.anchorRootRadiusSubvoxels = std::max(recipe.anchorRootRadiusSubvoxels,
|
|
||||||
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
||||||
+ + floating_island_detail::Q);
|
|
||||||
+ recipe.anchorIslandMinimumRadiusSubvoxels = std::max(recipe.minimumRadiusSubvoxels,
|
|
||||||
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
|
|
||||||
+ + floating_island_detail::Q + 1);
|
|
||||||
+ // Keep the resolved fixed mouth on a capsule-grade ramp at the anchor island lip.
|
|
||||||
+ recipe.topFlattenQ16 = std::max<std::int64_t>(recipe.topFlattenQ16, 52429);
|
|
||||||
+ recipe.bridgeHalfWidthSubvoxels = std::max(recipe.bridgeHalfWidthSubvoxels,
|
|
||||||
+ recipe.routeConstraints.capsuleRadiusSubvoxels + 2 * floating_island_detail::Q);
|
|
||||||
+ if (!floating_island_detail::ValidateRecipe(recipe, error)) return std::nullopt;
|
|
||||||
+
|
|
||||||
+ auto& islands = manifest.plan.islands;
|
|
||||||
+ auto& edges = manifest.plan.clusterEdges;
|
|
||||||
+ auto& route = manifest.plan.route;
|
|
||||||
+ constexpr std::uint32_t count = 9;
|
|
||||||
+ double tangentX = endpointDistance > 1.0
|
|
||||||
+ ? dx / endpointDistance : 1.0;
|
|
||||||
+ double tangentY = endpointDistance > 1.0
|
|
||||||
+ ? dy / endpointDistance : 0.0;
|
|
||||||
+ if (endpointDistance <= 1.0)
|
|
||||||
+ {
|
|
||||||
+ const double angle = static_cast<double>(floating_island_detail::UnitQ16(
|
|
||||||
+ recipe.seed, 0x900000000ull, 0x524f555445ull))
|
|
||||||
+ * (2.0 * floating_island_detail::Pi / 65536.0);
|
|
||||||
+ tangentX = std::cos(angle);
|
|
||||||
+ tangentY = std::sin(angle);
|
|
||||||
+ }
|
|
||||||
+ const double normalX = -tangentY;
|
|
||||||
+ const double normalY = tangentX;
|
|
||||||
+ const auto localPoint = [&](double along, double side)
|
|
||||||
+ {
|
|
||||||
+ return std::array<std::int64_t, 2>{
|
|
||||||
+ floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(endpoints.arrivalSubvoxels[0])
|
|
||||||
+ + tangentX * along + normalX * side),
|
|
||||||
+ floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(endpoints.arrivalSubvoxels[1])
|
|
||||||
+ + tangentY * along + normalY * side)};
|
|
||||||
+ };
|
|
||||||
+ std::array<std::array<std::int64_t, 2>, count> primaryXY{};
|
|
||||||
+ primaryXY[0] = {endpoints.arrivalSubvoxels[0], endpoints.arrivalSubvoxels[1]};
|
|
||||||
+ primaryXY[1] = localPoint(0.0, static_cast<double>(step));
|
|
||||||
+ primaryXY[2] = localPoint(static_cast<double>(step), static_cast<double>(step));
|
|
||||||
+ primaryXY[3] = localPoint(static_cast<double>(step), 0.0);
|
|
||||||
+ primaryXY[4] = localPoint(static_cast<double>(step), -static_cast<double>(step));
|
|
||||||
+ primaryXY[5] = localPoint(0.0, -static_cast<double>(step));
|
|
||||||
+ primaryXY[6] = localPoint(-static_cast<double>(step), -static_cast<double>(step));
|
|
||||||
+ // Keep the return lane outside the arrival passage's spiral and on the negative
|
|
||||||
+ // side of the first connector, so the two bridges cannot cross in plan view.
|
|
||||||
+ // Keep the final approach radial at the passage mouth. This lets the shared
|
|
||||||
+ // passage resolver keep its authored route inside the placement disk.
|
|
||||||
+ primaryXY[7] = localPoint(-0.85 * static_cast<double>(step),
|
|
||||||
+ -0.23 * static_cast<double>(step));
|
|
||||||
+ const double departureRadial = std::hypot(
|
|
||||||
+ static_cast<double>(endpoints.departureSubvoxels[0]),
|
|
||||||
+ static_cast<double>(endpoints.departureSubvoxels[1]));
|
|
||||||
+ const double anchorScale = departureRadial > 1.0
|
|
||||||
+ ? std::max(0.0, (departureRadial - recipe.anchorMouthOffsetSubvoxels) / departureRadial)
|
|
||||||
+ : 0.0;
|
|
||||||
+ primaryXY[8] = {
|
|
||||||
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[0] * anchorScale),
|
|
||||||
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[1] * anchorScale)};
|
|
||||||
+
|
|
||||||
+ std::array<double, count> cumulative{};
|
|
||||||
+ for (std::uint32_t i = 1; i < count; ++i)
|
|
||||||
+ {
|
|
||||||
+ const double segmentX = static_cast<double>(primaryXY[i][0] - primaryXY[i - 1][0]);
|
|
||||||
+ const double segmentY = static_cast<double>(primaryXY[i][1] - primaryXY[i - 1][1]);
|
|
||||||
+ const double length = std::hypot(segmentX, segmentY);
|
|
||||||
+ if (!(length > 0.0) || length > static_cast<double>(256 * floating_island_detail::Q))
|
|
||||||
+ { error = "resolved island mouths require an unsupported serpentine segment length"; return std::nullopt; }
|
|
||||||
+ cumulative[i] = cumulative[i - 1] + length;
|
|
||||||
+ }
|
|
||||||
+ const double totalLength = cumulative.back();
|
|
||||||
+ if (!(totalLength > 0.0))
|
|
||||||
+ { error = "resolved floating-island mouths do not define a route"; return std::nullopt; }
|
|
||||||
+ const std::int64_t capsuleHalf = recipe.routeConstraints.capsuleHeightSubvoxels / 2;
|
|
||||||
+ const std::int64_t routeOffset = capsuleHalf
|
|
||||||
+ + recipe.routeConstraints.minimumClearanceSubvoxels
|
|
||||||
+ + floating_island_detail::Q / 8;
|
|
||||||
+ const std::int64_t startTop = endpoints.arrivalSubvoxels[2] - routeOffset;
|
|
||||||
+ const std::int64_t endTop = endpoints.departureSubvoxels[2] - routeOffset;
|
|
||||||
+ const std::uint64_t anchorSite = 0x700000000ull + recipe.anchorIslandOrdinal;
|
|
||||||
+ auto anchorShape = floating_island_detail::MakeIsland(recipe,
|
|
||||||
+ recipe.anchorIslandOrdinal, primaryXY.back()[0], primaryXY.back()[1], endTop, anchorSite);
|
|
||||||
+ anchorShape.radiusSubvoxels = std::max(anchorShape.radiusSubvoxels,
|
|
||||||
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
||||||
+ anchorShape.topHalfSubvoxels = std::max<std::int64_t>(1,
|
|
||||||
+ anchorShape.radiusSubvoxels / 5);
|
|
||||||
+ anchorShape.centerSubvoxels[2] = endTop - anchorShape.topHalfSubvoxels;
|
|
||||||
+ std::int64_t anchorTop = endTop;
|
|
||||||
+ for (int iteration = 0; iteration < 4; ++iteration)
|
|
||||||
+ {
|
|
||||||
+ anchorShape.topZSubvoxels = anchorTop;
|
|
||||||
+ anchorShape.centerSubvoxels[2] = anchorTop - anchorShape.topHalfSubvoxels;
|
|
||||||
+ const double mouthSurface = floating_island_detail::IslandTopSurface(recipe,
|
|
||||||
+ anchorShape, static_cast<double>(endpoints.departureSubvoxels[0]),
|
|
||||||
+ static_cast<double>(endpoints.departureSubvoxels[1]));
|
|
||||||
+ anchorTop = endTop + floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(anchorTop) - mouthSurface);
|
|
||||||
+ }
|
|
||||||
+ std::array<std::int64_t, count> tops{};
|
|
||||||
+ for (std::uint32_t i = 0; i < count; ++i)
|
|
||||||
+ {
|
|
||||||
+ const double t = cumulative[i] / totalLength;
|
|
||||||
+ tops[i] = floating_island_detail::RoundQ(static_cast<double>(startTop)
|
|
||||||
+ + static_cast<double>(anchorTop - startTop) * t);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ islands.reserve(count);
|
|
||||||
+ for (std::uint32_t i = 0; i < count; ++i)
|
|
||||||
+ {
|
|
||||||
+ const std::uint64_t site = 0x700000000ull + i;
|
|
||||||
+ auto island = floating_island_detail::MakeIsland(recipe, i,
|
|
||||||
+ primaryXY[i][0], primaryXY[i][1], tops[i], site);
|
|
||||||
+ if (i == recipe.anchorIslandOrdinal)
|
|
||||||
+ {
|
|
||||||
+ island.radiusSubvoxels = std::max(island.radiusSubvoxels,
|
|
||||||
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
|
||||||
+ }
|
|
||||||
+ const std::int64_t interiorBottom = recipe.layoutMinimumZSubvoxels
|
|
||||||
+ + recipe.boundarySealSubvoxels;
|
|
||||||
+ while (island.radiusSubvoxels > recipe.minimumRadiusSubvoxels
|
|
||||||
+ && floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
|
|
||||||
+ < interiorBottom)
|
|
||||||
+ --island.radiusSubvoxels;
|
|
||||||
+ const std::int64_t requiredRadius = i == recipe.anchorIslandOrdinal
|
|
||||||
+ ? recipe.anchorIslandMinimumRadiusSubvoxels : recipe.minimumRadiusSubvoxels;
|
|
||||||
+ if (island.radiusSubvoxels < requiredRadius
|
|
||||||
+ || floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
|
|
||||||
+ < interiorBottom)
|
|
||||||
+ {
|
|
||||||
+ error = "resolved-mouth island " + std::to_string(i) + " cannot fit above sealed bedrock"
|
|
||||||
+ + " (radius=" + std::to_string(island.radiusSubvoxels)
|
|
||||||
+ + ", required=" + std::to_string(requiredRadius)
|
|
||||||
+ + ", top=" + std::to_string(tops[i])
|
|
||||||
+ + ", bottom=" + std::to_string(floating_island_detail::IslandBottom(
|
|
||||||
+ recipe, tops[i], island.radiusSubvoxels))
|
|
||||||
+ + ", interior_bottom=" + std::to_string(interiorBottom)
|
|
||||||
+ + ", anchor_top=" + std::to_string(anchorTop)
|
|
||||||
+ + ", mouth_offset=" + std::to_string(recipe.anchorMouthOffsetSubvoxels) + ")";
|
|
||||||
+ return std::nullopt;
|
|
||||||
+ }
|
|
||||||
+ island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
|
|
||||||
+ island.topZSubvoxels = tops[i];
|
|
||||||
+ island.centerSubvoxels[2] = tops[i] - island.topHalfSubvoxels;
|
|
||||||
+ island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe,
|
|
||||||
+ island.topZSubvoxels, island.radiusSubvoxels);
|
|
||||||
+ floating_island_detail::SetIslandBounds(recipe, island);
|
|
||||||
+ islands.push_back(std::move(island));
|
|
||||||
+ }
|
|
||||||
+ for (std::uint32_t i = 0; i + 1 < count; ++i)
|
|
||||||
+ edges.push_back(floating_island_detail::MakeConnector(recipe,
|
|
||||||
+ islands[i], islands[i + 1], 0x600000000ull + i));
|
|
||||||
+
|
|
||||||
+ FloatingIslandFieldV1 planningField(recipe, manifest.plan);
|
|
||||||
+ if (!planningField.isValid())
|
|
||||||
+ { error = "resolved-mouth island field did not pass the Core field contract"; return std::nullopt; }
|
|
||||||
+ const std::int64_t routeRadius = 4 * floating_island_detail::Q;
|
|
||||||
+ std::uint32_t ordinal = 0;
|
|
||||||
+ for (std::uint32_t segment = 0; segment + 1 < count; ++segment)
|
|
||||||
+ {
|
|
||||||
+ const double segmentX = static_cast<double>(primaryXY[segment + 1][0]
|
|
||||||
+ - primaryXY[segment][0]);
|
|
||||||
+ const double segmentY = static_cast<double>(primaryXY[segment + 1][1]
|
|
||||||
+ - primaryXY[segment][1]);
|
|
||||||
+ const double length = std::hypot(segmentX, segmentY);
|
|
||||||
+ const std::uint32_t pieces = static_cast<std::uint32_t>(std::max(1.0,
|
|
||||||
+ std::ceil(length / (4.0 * floating_island_detail::Q))));
|
|
||||||
+ for (std::uint32_t piece = segment == 0 ? 0 : 1; piece <= pieces; ++piece)
|
|
||||||
+ {
|
|
||||||
+ const double t = static_cast<double>(piece) / pieces;
|
|
||||||
+ const auto x = floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(primaryXY[segment][0]) + segmentX * t);
|
|
||||||
+ const auto y = floating_island_detail::RoundQ(
|
|
||||||
+ static_cast<double>(primaryXY[segment][1]) + segmentY * t);
|
|
||||||
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
|
||||||
+ recipe, manifest.plan, x, y);
|
|
||||||
+ if (!surface)
|
|
||||||
+ { error = "resolved-mouth island chain has no capsule-width support along a connector"; return std::nullopt; }
|
|
||||||
+ WorldAssemblyRoutePointV1 point;
|
|
||||||
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
|
||||||
+ 0x800000000ull + ordinal, 0x524f555445ull);
|
|
||||||
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
|
|
||||||
+ point.radiusSubvoxels = routeRadius;
|
|
||||||
+ if (segment == 0 && piece == 0)
|
|
||||||
+ point.positionSubvoxels = endpoints.arrivalSubvoxels;
|
|
||||||
+ route.push_back(point);
|
|
||||||
+ ++ordinal;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ const double mouthLegX = static_cast<double>(endpoints.departureSubvoxels[0]
|
|
||||||
+ - primaryXY.back()[0]);
|
|
||||||
+ const double mouthLegY = static_cast<double>(endpoints.departureSubvoxels[1]
|
|
||||||
+ - primaryXY.back()[1]);
|
|
||||||
+ const double mouthLegLength = std::hypot(mouthLegX, mouthLegY);
|
|
||||||
+ const std::uint32_t mouthLegPieces = static_cast<std::uint32_t>(std::max(1.0,
|
|
||||||
+ std::ceil(mouthLegLength / (4.0 * floating_island_detail::Q))));
|
|
||||||
+ for (std::uint32_t piece = 1; piece <= mouthLegPieces; ++piece)
|
|
||||||
+ {
|
|
||||||
+ const double t = static_cast<double>(piece) / mouthLegPieces;
|
|
||||||
+ const auto x = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[0])
|
|
||||||
+ + mouthLegX * t);
|
|
||||||
+ const auto y = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[1])
|
|
||||||
+ + mouthLegY * t);
|
|
||||||
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
|
||||||
+ recipe, manifest.plan, x, y);
|
|
||||||
+ if (!surface)
|
|
||||||
+ { error = "resolved island passage mouth has no capsule-width support along its anchor landing at x="
|
|
||||||
+ + std::to_string(x) + ", y=" + std::to_string(y) + ", piece="
|
|
||||||
+ + std::to_string(piece) + "/" + std::to_string(mouthLegPieces)
|
|
||||||
+ + ", anchor_radius=" + std::to_string(anchorShape.radiusSubvoxels); return std::nullopt; }
|
|
||||||
+ WorldAssemblyRoutePointV1 point;
|
|
||||||
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
|
||||||
+ 0x800000000ull + ordinal, 0x524f555445ull);
|
|
||||||
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
|
|
||||||
+ point.radiusSubvoxels = routeRadius;
|
|
||||||
+ if (piece == mouthLegPieces)
|
|
||||||
+ point.positionSubvoxels = endpoints.departureSubvoxels;
|
|
||||||
+ route.push_back(point);
|
|
||||||
+ ++ordinal;
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
+ const double maximumGrade = floating_island_detail::Q16ToDouble(
|
|
||||||
+ recipe.routeConstraints.maximumGradeQ16) * 0.95;
|
|
||||||
+ std::vector<std::int64_t> baseHeights;
|
|
||||||
+ std::vector<double> routeDistance(route.size(), 0.0);
|
|
||||||
+ baseHeights.reserve(route.size());
|
|
||||||
+ for (std::size_t i = 0; i < route.size(); ++i)
|
|
||||||
+ {
|
|
||||||
+ baseHeights.push_back(route[i].positionSubvoxels[2]);
|
|
||||||
+ if (i > 0)
|
|
||||||
+ {
|
|
||||||
+ const double dx = static_cast<double>(route[i].positionSubvoxels[0]
|
|
||||||
+ - route[i - 1].positionSubvoxels[0]);
|
|
||||||
+ const double dy = static_cast<double>(route[i].positionSubvoxels[1]
|
|
||||||
+ - route[i - 1].positionSubvoxels[1]);
|
|
||||||
+ routeDistance[i] = routeDistance[i - 1] + std::hypot(dx, dy);
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ for (std::size_t i = 0; i < route.size(); ++i)
|
|
||||||
+ {
|
|
||||||
+ double profile = static_cast<double>(baseHeights[i]);
|
|
||||||
+ std::size_t profileSource = i;
|
|
||||||
+ for (std::size_t j = 0; j < route.size(); ++j)
|
|
||||||
+ {
|
|
||||||
+ const double pathDistance = std::abs(routeDistance[i] - routeDistance[j]);
|
|
||||||
+ const double candidate = static_cast<double>(baseHeights[j])
|
|
||||||
+ - maximumGrade * pathDistance;
|
|
||||||
+ if (candidate > profile)
|
|
||||||
+ {
|
|
||||||
+ profile = candidate;
|
|
||||||
+ profileSource = j;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ const std::int64_t plannedHeight = floating_island_detail::RoundQ(profile);
|
|
||||||
+ const bool fixedEndpoint = i == 0 || i + 1 == route.size();
|
|
||||||
+ if (fixedEndpoint && plannedHeight != baseHeights[i])
|
|
||||||
+ {
|
|
||||||
+ error = "resolved fixed island mouth endpoint " + std::to_string(i)
|
|
||||||
+ + " needs " + std::to_string(plannedHeight - baseHeights[i])
|
|
||||||
+ + " extra subvoxels from route sample " + std::to_string(profileSource)
|
|
||||||
+ + " at z=" + std::to_string(baseHeights[profileSource])
|
|
||||||
+ + " and path distance=" + std::to_string(static_cast<std::int64_t>(
|
|
||||||
+ std::abs(routeDistance[i] - routeDistance[profileSource])))
|
|
||||||
+ + " (arrival_z="
|
|
||||||
+ + std::to_string(endpoints.arrivalSubvoxels[2]) + ", departure_z="
|
|
||||||
+ + std::to_string(endpoints.departureSubvoxels[2]) + ", path_length="
|
|
||||||
+ + std::to_string(static_cast<std::int64_t>(routeDistance.back())) + ")";
|
|
||||||
+ return std::nullopt;
|
|
||||||
+ }
|
|
||||||
+ route[i].positionSubvoxels[2] = plannedHeight;
|
|
||||||
+ }
|
|
||||||
+ if (!manifest.finalize(error)) return std::nullopt;
|
|
||||||
+ const std::string encoded = manifest.ToJson();
|
|
||||||
+ const auto roundTrip = FloatingIslandManifestV1::FromJson(encoded, error);
|
|
||||||
+ if (!roundTrip || roundTrip->ToJson() != encoded
|
|
||||||
+ || roundTrip->contentHash != manifest.contentHash)
|
|
||||||
+ { if (error.empty()) error = "resolved-mouth floating-island plan did not canonical-round-trip"; return std::nullopt; }
|
|
||||||
+ return manifest;
|
|
||||||
+}
|
|
||||||
+
|
|
||||||
FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe,
|
|
||||||
const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan)
|
|
||||||
{}
|
|
||||||
diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
||||||
index a0cb0a7..468e315 100644
|
|
||||||
--- a/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
||||||
+++ b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
|
||||||
@@ -1826,12 +1826,59 @@ int main(int argc,char** argv)
|
|
||||||
else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
|
||||||
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
|
|
||||||
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
|
||||||
- if(((!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
|
||||||
- ||(options.NativeOwnerFloatingIslandsRecipe
|
|
||||||
- &&worldAssembly.floatingIslands.empty()))
|
|
||||||
- &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
||||||
- *ownerWorld,ownerJsonRoot,worldAssembly,error))
|
|
||||||
- {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
||||||
+ const bool attachFloatingIslandPlan=(!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
|
||||||
+ ||(options.NativeOwnerFloatingIslandsRecipe&&worldAssembly.floatingIslands.empty());
|
|
||||||
+ if(attachFloatingIslandPlan)
|
|
||||||
+ {
|
|
||||||
+ const FStrateSlot* islandSlot=nullptr;
|
|
||||||
+ if(ownerWorld->Manager)
|
|
||||||
+ for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
|
|
||||||
+ if(slot.Definition&&slot.Definition->GeneratorType==ECaveGeneratorType::FloatingIslands)
|
|
||||||
+ {islandSlot=&slot;break;}
|
|
||||||
+ if(!islandSlot)
|
|
||||||
+ {std::fprintf(stderr,"Floating-island endpoint resolution found no FloatingIslands strate.\n");return 2;}
|
|
||||||
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 mouthLayout;
|
|
||||||
+ if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
|
||||||
+ *ownerWorld,worldAssembly,"floating-islands",mouthLayout,error))
|
|
||||||
+ {std::fprintf(stderr,"Pre-plan passage layout failed: %s\n",error.c_str());return 2;}
|
|
||||||
+ VoxelForgeCore::FloatingIslandRouteEndpointsV1 endpoints;
|
|
||||||
+ bool hasArrival=false,hasDeparture=false;
|
|
||||||
+ for(const auto& passage:mouthLayout.passages)
|
|
||||||
+ {
|
|
||||||
+ const std::size_t lastSlash=passage.stableId.rfind('/');
|
|
||||||
+ if(lastSlash==std::string::npos||passage.stableId.substr(lastSlash+1)!="0")continue;
|
|
||||||
+ if(passage.lowerStrateIndex==islandSlot->StrateIndex&&!hasArrival)
|
|
||||||
+ {
|
|
||||||
+ endpoints.arrivalSubvoxels=passage.lowerMouthSubvoxels;
|
|
||||||
+ hasArrival=true;
|
|
||||||
+ }
|
|
||||||
+ if(passage.upperStrateIndex==islandSlot->StrateIndex&&!hasDeparture)
|
|
||||||
+ {
|
|
||||||
+ endpoints.departureSubvoxels=passage.upperMouthSubvoxels;
|
|
||||||
+ hasDeparture=true;
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
+ if(!hasArrival||!hasDeparture)
|
|
||||||
+ {std::fprintf(stderr,"Resolved passage layout does not provide both FI route mouths (arrival=%s departure=%s).\n",
|
|
||||||
+ hasArrival?"yes":"no",hasDeparture?"yes":"no");return 2;}
|
|
||||||
+ std::printf("[FloatingIslandResolvedMouths] strate=%d arrival_m=(%.3f,%.3f,%.3f) departure_m=(%.3f,%.3f,%.3f) status=PASS\n",
|
|
||||||
+ islandSlot->StrateIndex,
|
|
||||||
+ endpoints.arrivalSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.arrivalSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.arrivalSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.departureSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.departureSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
|
||||||
+ endpoints.departureSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
|
|
||||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
|
||||||
+ if(!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
|
||||||
+ *ownerWorld,ownerJsonRoot,worldAssembly,endpoints,error))
|
|
||||||
+ {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
|
||||||
+ }
|
|
||||||
if(!options.WorldAssemblyManifestOutputPath.empty())
|
|
||||||
{
|
|
||||||
const std::string manifestText=worldAssembly.ToJson();
|
|
||||||
diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h
|
|
||||||
index dbe2a7c..9790f2f 100644
|
|
||||||
--- a/Standalone/Source/WorldAssemblyPrototype.h
|
|
||||||
+++ b/Standalone/Source/WorldAssemblyPrototype.h
|
|
||||||
@@ -923,7 +923,8 @@ namespace StandaloneWorldAssemblyPrototype
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
|
|
||||||
- VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error)
|
|
||||||
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
|
|
||||||
+ const VoxelForgeCore::FloatingIslandRouteEndpointsV1& endpoints,std::string& error)
|
|
||||||
{
|
|
||||||
if(!owner.Manager)
|
|
||||||
{error="floating-island assembly needs the resolved standalone strate manager";return false;}
|
|
||||||
@@ -996,7 +997,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|
||||||
recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels
|
|
||||||
+recipe.boundarySealSubvoxels;
|
|
||||||
|
|
||||||
- auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
|
|
||||||
+ auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,endpoints,error);
|
|
||||||
if(!planned)return false;
|
|
||||||
const VoxelForgeCore::FloatingIslandFieldV1 routeField(
|
|
||||||
planned->recipe,planned->plan);
|
|
||||||
@@ -1217,6 +1218,7 @@ namespace StandaloneWorldAssemblyPrototype
|
|
||||||
:it->manifest.plan.route.size()-1;
|
|
||||||
std::size_t resolvedRoutePointCount=it->manifest.plan.route.size();
|
|
||||||
if(it->manifest.recipe.geometryVersion>=5
|
|
||||||
+ &&it->manifest.recipe.geometryVersion<9
|
|
||||||
&&!it->manifest.plan.route.empty()
|
|
||||||
&&it->manifest.recipe.anchorIslandOrdinal<it->manifest.plan.islands.size())
|
|
||||||
{
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# L157 — near terrain at 30 m/s
|
||||||
|
|
||||||
|
**Result:** Early admission raised moving near coverage, but the W8 experiment did not meet the full latency and worst-tick bar. Keep both experiment controls disabled by default. The UE host was built from `/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l157-tree/Source`; the VFHost source symlink has since been restored.
|
||||||
|
|
||||||
|
## Measurements
|
||||||
|
|
||||||
|
Five interleaved pairs used the same 30 m/s, 12 s movement plus 5 s dwell, 8 effective tile slots, and four-strate owner plan. Each run had 45–46 moving samples. The uncounted candidate smoke run warmed the same UE binary. The machine was quiet before the matrix (uptime load averages 1.09/1.40/1.22).
|
||||||
|
|
||||||
|
| Moving metric | `assembler/world` median | Early admission + 1 ms cap median | Paired median delta |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| LOD0–2 ready | 36.06% | 48.64% | +10.76 pp |
|
||||||
|
| 55° forward-cone ready | 12.12% | 16.86% | +5.95 pp |
|
||||||
|
| Game-thread tick p99 | 12.51 ms | 10.93 ms | +0.07 ms |
|
||||||
|
| Worst tick per run, median | 14.67 ms | 14.52 ms | +0.18 ms |
|
||||||
|
| Far holes | 4.00% | 5.82% | −0.13 pp |
|
||||||
|
| Initial near-set arrival | 2.80 s | 3.21 s | +0.01 s |
|
||||||
|
|
||||||
|
The p99 median is under 11 ms, but only three candidate runs were under that limit; the other two were 15.75 and 18.17 ms. The worst observed candidate tick was 19.88 ms versus 17.75 ms for baseline. Far-hole results were mixed: the paired median improved slightly, while two candidate pairs regressed and the worst candidate run had 12.48% holes versus a 7.08% baseline worst. Arrival was effectively tied by paired median, with one candidate pair 0.88 s slower. The combined bar therefore fails on repeatability and worst tick; far holes are not robustly no-worse.
|
||||||
|
|
||||||
|
The apply cap lowered median apply p99 from 2.11 to 1.40 ms, but it is not a hard per-frame ceiling: candidate apply p99 reached 6.61 ms and the maximum was 7.58 ms. The loop checks elapsed time after finishing a result. Candidate stream-update p99 reached 15.07 and 13.52 ms in the same two outlier runs. Historical L122 frame joins likewise show that applying extra results adds work on candidate tail frames (about 1.0–1.5 ms), while `UpdateChunks`/desired-set work forms the larger tail; `level_admit` and `submit_tiles` p99 were not consistently higher. The cap cannot make the whole tick fit when update work already exceeds the frame budget.
|
||||||
|
|
||||||
|
## Direction and next lever
|
||||||
|
|
||||||
|
The old metric counts every desired LOD0–2 tile equally, including tiles behind the player. Desired tiles split about 50.44% ahead and 49.56% behind; only 23.20% fell inside the 55° forward cone. Behind readiness was 50.93% baseline / 68.49% candidate, versus 21.71% / 30.60% ahead. The all-around count therefore overstates what is ready in the direction of travel.
|
||||||
|
|
||||||
|
For VR, readiness against the HMD/camera frustum is the useful visibility metric. The velocity-forward cone measured here is a movement proxy, not a head-pose measurement. Report it beside the old all-around residency count. No additional speed fix is validated; a narrow forward-cone admission probe is the next cheap experiment, while desired-set rebuild/update work remains the measured latency target.
|
||||||
|
|
||||||
|
## Checks and limits
|
||||||
|
|
||||||
|
- UBT built `VFHostEditor` successfully, 98/98 actions, using `-NoUBA -MaxParallelActions=4`. It compiled `Source/VoxelForge/Private/VoxelWorld.cpp` from the L157 worktree. Three existing RealtimeMesh deprecation warnings remained.
|
||||||
|
- Fast tier: 268 PASS, 0 FAIL, 5 older-baseline diagnostics. Current canonical owner hashes match L130; W1/W12 hashes match for both pinned owners. Random-camera tile hashes also match across W1/W8/W12.
|
||||||
|
- UE `TileHashDump` comparison was inconclusive: both runs logged `records=0`, despite streamed meshes. No direct UE tile-hash claim is made. The L157 changes are limited to admission/apply controls and trace accounting; no generator or mesher source changed.
|
||||||
|
- W12 movement timing and HMD-pose visibility were not run. W12 was skipped after W8 failed the worst-tick bar; direct UE hash capture emitted no records.
|
||||||
|
- `RunL157Movement.py` passed `py_compile`; `git diff --check` passed.
|
||||||
|
|
||||||
|
Logs and paired data are in `Saved/Linux/l157/`; movement traces and summary are in `Saved/Linux/l157/ue/movement/`.
|
||||||
|
|
||||||
|
Changed Unreal source: `Source/VoxelForge/Private/VoxelWorld.cpp`.
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Interleaved L157 UE movement runs for critical-gate and apply-budget ablations."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
HOST = Path("/home/jahni/VFHost")
|
||||||
|
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||||
|
OUT = ROOT / "Saved/Linux/l157/ue/movement"
|
||||||
|
HOST_SAVED = HOST / "Saved/Linux/l157"
|
||||||
|
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l157"
|
||||||
|
SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375)
|
||||||
|
MOVE_START_S = 6.0
|
||||||
|
|
||||||
|
CONDITIONS = {
|
||||||
|
"assembler_world": {"wait_for_critical": 1, "apply_budget_ms": 0.0},
|
||||||
|
"early_admission_apply_1ms": {"wait_for_critical": 0, "apply_budget_ms": 1.0},
|
||||||
|
}
|
||||||
|
ORDER = {
|
||||||
|
1: ("assembler_world", "early_admission_apply_1ms"),
|
||||||
|
2: ("early_admission_apply_1ms", "assembler_world"),
|
||||||
|
3: ("assembler_world", "early_admission_apply_1ms"),
|
||||||
|
4: ("early_admission_apply_1ms", "assembler_world"),
|
||||||
|
5: ("assembler_world", "early_admission_apply_1ms"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def percentile(values: list[float], fraction: float = 0.99) -> float | None:
|
||||||
|
if not values:
|
||||||
|
return None
|
||||||
|
ordered = sorted(values)
|
||||||
|
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
|
||||||
|
|
||||||
|
|
||||||
|
def read_events(path: Path) -> list[dict]:
|
||||||
|
rows = []
|
||||||
|
if path.is_file():
|
||||||
|
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||||
|
if not line.startswith("{"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rows.append(json.loads(line))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def mean(values: list[float]) -> float | None:
|
||||||
|
return statistics.mean(values) if values else None
|
||||||
|
|
||||||
|
|
||||||
|
def percent(numerator: int | float, denominator: int | float) -> float | None:
|
||||||
|
return 100.0 * numerator / denominator if denominator > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def run(condition: str, repetition: int, tile_hash_dump: Path | None = None) -> dict:
|
||||||
|
config = CONDITIONS[condition]
|
||||||
|
run_name = f"fast-r{repetition}"
|
||||||
|
out = OUT / condition
|
||||||
|
host_out = HOST_PLUGIN_SAVED / condition / "output"
|
||||||
|
userdir = HOST_SAVED / "user" / condition / run_name
|
||||||
|
zendata = HOST_SAVED / "zen" / condition / run_name
|
||||||
|
ddc = HOST_SAVED / "DDC"
|
||||||
|
trace = out / f"{run_name}.trace.jsonl"
|
||||||
|
abslog = out / f"{run_name}.log"
|
||||||
|
stdout = out / f"{run_name}.stdout.log"
|
||||||
|
export = host_out / run_name
|
||||||
|
for directory in (out, export, userdir, zendata, ddc):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
trace.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M)
|
||||||
|
command = [
|
||||||
|
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||||
|
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||||
|
"-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer",
|
||||||
|
"-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}",
|
||||||
|
f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0",
|
||||||
|
"-archetype=TunnelNetwork", "-slot=0", "-modes=export", "-gameconfig=1",
|
||||||
|
"-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25",
|
||||||
|
"-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0",
|
||||||
|
"-coreplantest=0", "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings",
|
||||||
|
"-corehybridmesher=0", f"-out={export}", "-voxel.StreamingCoarseFirst=1",
|
||||||
|
"-voxel.StreamingDynamicNearReserve=1",
|
||||||
|
"-voxel.StreamingLegacyPendingResultCap=0",
|
||||||
|
"-voxel.CoreRegionConcurrentAdmission=1",
|
||||||
|
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||||
|
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||||
|
"-voxel.StreamingFarApplySupplement=1",
|
||||||
|
f"-voxel.StreamingFastTravelWaitForAllCriticalTiles={config['wait_for_critical']}",
|
||||||
|
f"-voxel.StreamingFastTravelApplyBudgetMilliseconds={config['apply_budget_ms']:.1f}",
|
||||||
|
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||||
|
"-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=3000.0",
|
||||||
|
f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}",
|
||||||
|
"-voxel.TestMoveDurationSeconds=12.0", "-voxel.TestExitSeconds=23.0",
|
||||||
|
"-voxel.TestMoveDirection=1,0,0", "-voxel.StreamingSchedulerPolicy=3",
|
||||||
|
"-voxel.TestExitOnSteady=0", "-coreworldstreamtrace=1",
|
||||||
|
f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||||
|
"-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1",
|
||||||
|
"-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}",
|
||||||
|
f"-abslog={abslog}",
|
||||||
|
]
|
||||||
|
if tile_hash_dump is not None:
|
||||||
|
tile_hash_dump.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tile_hash_dump.unlink(missing_ok=True)
|
||||||
|
command.append(f"-voxel.TileHashDump={tile_hash_dump.resolve()}")
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
|
||||||
|
started = time.monotonic()
|
||||||
|
with stdout.open("w", encoding="utf-8") as stream:
|
||||||
|
process = subprocess.run(
|
||||||
|
wrapped, cwd=ROOT, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
check=False, timeout=1740)
|
||||||
|
|
||||||
|
events = read_events(trace)
|
||||||
|
samples = [event for event in events if event.get("type") == "streaming_sample"]
|
||||||
|
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
|
||||||
|
stop_event = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_test_move_stopped"), None)
|
||||||
|
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
|
||||||
|
worker_cap = next((event for event in events
|
||||||
|
if event.get("type") == "streaming_worker_cap_readback"), {})
|
||||||
|
if stop_event is None:
|
||||||
|
raise RuntimeError(f"missing move-stop event: {trace}")
|
||||||
|
stop_s = float(stop_event.get("t_s", 0.0))
|
||||||
|
move_start_s = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
|
||||||
|
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < move_start_s
|
||||||
|
and float(event.get("speed_cm_s", 0.0)) < 50.0]
|
||||||
|
if process.returncode or len(moving) < 40 or len(dwell) < 16:
|
||||||
|
raise RuntimeError(f"incomplete {condition} run: {trace}; exit={process.returncode}; "
|
||||||
|
f"move={len(moving)} dwell={len(dwell)}")
|
||||||
|
if (int(backend.get("streaming_fast_travel_wait_for_all_critical_tiles", -1))
|
||||||
|
!= config["wait_for_critical"]
|
||||||
|
or abs(float(backend.get("streaming_fast_travel_apply_budget_ms", -1.0))
|
||||||
|
- config["apply_budget_ms"]) > 0.01
|
||||||
|
or int(worker_cap.get("streaming_background_worker_cap", -1)) != 1
|
||||||
|
or int(backend.get("effective_tile_slots", -1)) != 8
|
||||||
|
or int(backend.get("streaming_far_apply_supplement", -1)) != 1):
|
||||||
|
raise RuntimeError(f"CVar/worker readback mismatch: {backend}; {worker_cap}")
|
||||||
|
|
||||||
|
moving_metrics = []
|
||||||
|
for event in moving:
|
||||||
|
desired = event.get("desired_by_lod", [0] * 9)
|
||||||
|
resident = event.get("resident_by_lod", [0] * 9)
|
||||||
|
near_desired = sum(desired[:3])
|
||||||
|
cone_desired = int(event.get("near_forward_cone_desired", 0))
|
||||||
|
weighted_desired = float(event.get("near_forward_weighted_desired", 0.0))
|
||||||
|
moving_metrics.append({
|
||||||
|
"near_ready_pct": percent(sum(resident[:3]), near_desired) or 0.0,
|
||||||
|
"lod0_ready_pct": percent(resident[0], desired[0]) or 0.0,
|
||||||
|
"lod1_ready_pct": percent(resident[1], desired[1]) or 0.0,
|
||||||
|
"lod2_ready_pct": percent(resident[2], desired[2]) or 0.0,
|
||||||
|
"far_hole_pct": percent(
|
||||||
|
sum(desired[3:]) - sum(event.get("covered_by_lod", resident)[3:]),
|
||||||
|
sum(desired[3:])) or 0.0,
|
||||||
|
"ahead_share_pct": percent(int(event.get("near_ahead_desired", 0)), near_desired),
|
||||||
|
"behind_share_pct": percent(int(event.get("near_behind_desired", 0)), near_desired),
|
||||||
|
"side_share_pct": 100.0 - (
|
||||||
|
percent(int(event.get("near_ahead_desired", 0)), near_desired) or 0.0
|
||||||
|
) - (
|
||||||
|
percent(int(event.get("near_behind_desired", 0)), near_desired) or 0.0
|
||||||
|
),
|
||||||
|
"ahead_ready_pct": percent(
|
||||||
|
int(event.get("near_ahead_resident", 0)),
|
||||||
|
int(event.get("near_ahead_desired", 0))),
|
||||||
|
"behind_ready_pct": percent(
|
||||||
|
int(event.get("near_behind_resident", 0)),
|
||||||
|
int(event.get("near_behind_desired", 0))),
|
||||||
|
"forward_cone_ready_pct": percent(
|
||||||
|
int(event.get("near_forward_cone_resident", 0)), cone_desired),
|
||||||
|
"forward_cone_share_pct": percent(cone_desired, near_desired),
|
||||||
|
"forward_weighted_ready_pct": percent(
|
||||||
|
float(event.get("near_forward_weighted_resident", 0.0)), weighted_desired),
|
||||||
|
})
|
||||||
|
|
||||||
|
arrival_near_settled = next((event for event in arrival
|
||||||
|
if sum(event.get("desired_by_lod", [0] * 9)[:3]) > 0
|
||||||
|
and sum(event.get("settled_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])), None)
|
||||||
|
dwell_complete = next((event for event in dwell
|
||||||
|
if sum(event.get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
>= sum(event.get("desired_by_lod", [0] * 9)[:3])
|
||||||
|
and sum(event.get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
<= sum(event.get("covered_by_lod", [0] * 9)[3:])), None)
|
||||||
|
|
||||||
|
work = [event for event in events if event.get("type") == "game_thread_work_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s]
|
||||||
|
applies = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "apply_frame"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
scheduler = {event.get("frame"): event for event in events
|
||||||
|
if event.get("type") == "scheduler_admission"
|
||||||
|
and move_start_s <= float(event.get("t_s", 0.0)) < stop_s}
|
||||||
|
frames = []
|
||||||
|
for event in work:
|
||||||
|
apply = applies.get(event.get("frame"), {})
|
||||||
|
admit = scheduler.get(event.get("frame"), {})
|
||||||
|
frames.append({
|
||||||
|
"frame": event.get("frame"),
|
||||||
|
"tick_ms": float(event.get("tick_ms", 0.0)),
|
||||||
|
"update_chunks_ms": float(event.get("update_chunks_ms", 0.0)),
|
||||||
|
"pending_drain_ms": float(event.get("pending_drain_ms", 0.0)),
|
||||||
|
"apply_ms": float(apply.get("apply_ms", 0.0)),
|
||||||
|
"apply_budget_ms": float(apply.get("apply_budget_ms", 0.0)),
|
||||||
|
"meshes_applied": int(apply.get("meshes_applied", 0)),
|
||||||
|
"time_budget_hit": int(apply.get("time_limit_reached", 0)),
|
||||||
|
"desired_rebuild_ms": float(admit.get("desired_rebuild_ms", 0.0)),
|
||||||
|
"submit_tiles_ms": float(admit.get("submit_tiles_ms", 0.0)),
|
||||||
|
"level_admit_ms": float(admit.get("level_admit_ms", 0.0)),
|
||||||
|
})
|
||||||
|
p99_field = lambda key: percentile([float(row[key]) for row in frames])
|
||||||
|
max_field = lambda key: max((float(row[key]) for row in frames), default=None)
|
||||||
|
dwell_complete_s = (float(dwell_complete.get("t_s", 0.0)) - stop_s
|
||||||
|
if dwell_complete else None)
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"condition": condition,
|
||||||
|
"repetition": repetition,
|
||||||
|
"speed_mps": 30.0,
|
||||||
|
"wait_for_all_critical_tiles": config["wait_for_critical"],
|
||||||
|
"fast_travel_apply_budget_ms": config["apply_budget_ms"],
|
||||||
|
"effective_tile_slots": backend.get("effective_tile_slots"),
|
||||||
|
"exit_code": process.returncode,
|
||||||
|
"wall_s": round(time.monotonic() - started, 3),
|
||||||
|
"moving_samples": len(moving),
|
||||||
|
"dwell_samples": len(dwell),
|
||||||
|
"arrival_samples": len(arrival),
|
||||||
|
"near_ready_moving_mean_pct": mean([m["near_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod0_ready_moving_mean_pct": mean([m["lod0_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod1_ready_moving_mean_pct": mean([m["lod1_ready_pct"] for m in moving_metrics]),
|
||||||
|
"lod2_ready_moving_mean_pct": mean([m["lod2_ready_pct"] for m in moving_metrics]),
|
||||||
|
"far_holes_moving_mean_pct": mean([m["far_hole_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_desired_share_pct": mean([m["ahead_share_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_share_pct"] is not None]),
|
||||||
|
"near_behind_desired_share_pct": mean([m["behind_share_pct"] for m in moving_metrics
|
||||||
|
if m["behind_share_pct"] is not None]),
|
||||||
|
"near_side_desired_share_pct": mean([m["side_share_pct"] for m in moving_metrics]),
|
||||||
|
"near_ahead_ready_pct": mean([m["ahead_ready_pct"] for m in moving_metrics
|
||||||
|
if m["ahead_ready_pct"] is not None]),
|
||||||
|
"near_behind_ready_pct": mean([m["behind_ready_pct"] for m in moving_metrics
|
||||||
|
if m["behind_ready_pct"] is not None]),
|
||||||
|
"near_forward_cone_half_angle_deg": 55,
|
||||||
|
"near_forward_cone_desired_share_pct": mean([m["forward_cone_share_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_share_pct"] is not None]),
|
||||||
|
"near_forward_cone_ready_pct": mean([m["forward_cone_ready_pct"]
|
||||||
|
for m in moving_metrics
|
||||||
|
if m["forward_cone_ready_pct"] is not None]),
|
||||||
|
"near_forward_cosine_weighted_ready_pct": mean(
|
||||||
|
[m["forward_weighted_ready_pct"] for m in moving_metrics
|
||||||
|
if m["forward_weighted_ready_pct"] is not None]),
|
||||||
|
"arrival_lod02_settled_t_s": (
|
||||||
|
float(arrival_near_settled.get("t_s", 0.0)) if arrival_near_settled else None),
|
||||||
|
"near_ready_dwell_end_pct": (100.0 * sum(dwell[-1].get("resident_by_lod", [0] * 9)[:3])
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[:3]))),
|
||||||
|
"far_holes_dwell_end_pct": (100.0 * (
|
||||||
|
sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:])
|
||||||
|
- sum(dwell[-1].get("covered_by_lod", [0] * 9)[3:]))
|
||||||
|
/ max(1, sum(dwell[-1].get("desired_by_lod", [0] * 9)[3:]))),
|
||||||
|
"dwell_complete_after_stop_s": dwell_complete_s,
|
||||||
|
"game_thread_tick_p99_ms": p99_field("tick_ms"),
|
||||||
|
"game_thread_tick_max_ms": max_field("tick_ms"),
|
||||||
|
"stream_update_p99_ms": p99_field("update_chunks_ms"),
|
||||||
|
"pending_drain_p99_ms": p99_field("pending_drain_ms"),
|
||||||
|
"apply_p99_ms": p99_field("apply_ms"),
|
||||||
|
"apply_max_ms": max_field("apply_ms"),
|
||||||
|
"apply_budget_min_ms": min((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_budget_max_ms": max((f["apply_budget_ms"] for f in frames), default=None),
|
||||||
|
"apply_time_budget_hit_frames": sum(f["time_budget_hit"] for f in frames),
|
||||||
|
"apply_frames": sum(f["meshes_applied"] > 0 for f in frames),
|
||||||
|
"apply_meshes_total": sum(f["meshes_applied"] for f in frames),
|
||||||
|
"desired_rebuild_p99_ms": p99_field("desired_rebuild_ms"),
|
||||||
|
"submit_tiles_p99_ms": p99_field("submit_tiles_ms"),
|
||||||
|
"level_admit_p99_ms": p99_field("level_admit_ms"),
|
||||||
|
"top_tick_frames": sorted(frames, key=lambda f: f["tick_ms"], reverse=True)[:5],
|
||||||
|
"trace": str(trace.relative_to(ROOT)),
|
||||||
|
"log": str(abslog.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
print("[L157Movement] " + json.dumps(row, sort_keys=True), flush=True)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--smoke", action="store_true",
|
||||||
|
help="run one candidate trace to validate CVar readback and event fields")
|
||||||
|
parser.add_argument("--geometry-hash-check", action="store_true",
|
||||||
|
help="compare UE per-tile geometry hashes for baseline and early admission")
|
||||||
|
args = parser.parse_args()
|
||||||
|
missing = [path for path in (EDITOR, HOST / "VFHost.uproject") if not path.exists()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"missing UE runner input: {missing}")
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
if args.geometry_hash_check:
|
||||||
|
hash_dir = OUT / "geometry-hash"
|
||||||
|
baseline_path = hash_dir / "assembler-world.json"
|
||||||
|
candidate_path = hash_dir / "early-admission-apply-1ms.json"
|
||||||
|
run("assembler_world", 99, baseline_path)
|
||||||
|
run("early_admission_apply_1ms", 99, candidate_path)
|
||||||
|
baseline_data = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||||
|
candidate_data = json.loads(candidate_path.read_text(encoding="utf-8"))
|
||||||
|
key_fields = ("level", "tile_x", "tile_y", "tile_z")
|
||||||
|
|
||||||
|
def indexed(data: dict) -> dict[tuple[int, ...], tuple]:
|
||||||
|
result = {}
|
||||||
|
for record in data.get("records", []):
|
||||||
|
key = tuple(int(record[field]) for field in key_fields)
|
||||||
|
result[key] = (str(record.get("geometry_hash", "")),
|
||||||
|
int(record.get("triangle_count", -1)),
|
||||||
|
bool(record.get("empty", False)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
baseline_hashes = indexed(baseline_data)
|
||||||
|
candidate_hashes = indexed(candidate_data)
|
||||||
|
common = baseline_hashes.keys() & candidate_hashes.keys()
|
||||||
|
mismatches = [key for key in common
|
||||||
|
if baseline_hashes[key] != candidate_hashes[key]]
|
||||||
|
result = {
|
||||||
|
"baseline_records": len(baseline_hashes),
|
||||||
|
"candidate_records": len(candidate_hashes),
|
||||||
|
"common_records": len(common),
|
||||||
|
"hash_mismatches": len(mismatches),
|
||||||
|
"baseline_only_records": len(baseline_hashes.keys() - candidate_hashes.keys()),
|
||||||
|
"candidate_only_records": len(candidate_hashes.keys() - baseline_hashes.keys()),
|
||||||
|
"baseline_dump": str(baseline_path.relative_to(ROOT)),
|
||||||
|
"candidate_dump": str(candidate_path.relative_to(ROOT)),
|
||||||
|
}
|
||||||
|
(hash_dir / "comparison.json").write_text(
|
||||||
|
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
print("[L157GeometryHash] " + json.dumps(result, sort_keys=True), flush=True)
|
||||||
|
return 0 if len(common) >= 100 and not mismatches else 1
|
||||||
|
if args.smoke:
|
||||||
|
run("early_admission_apply_1ms", 0)
|
||||||
|
return 0
|
||||||
|
rows = []
|
||||||
|
partial = OUT / "runs-partial.json"
|
||||||
|
for repetition in range(1, 6):
|
||||||
|
for condition in ORDER[repetition]:
|
||||||
|
row = run(condition, repetition)
|
||||||
|
rows.append(row)
|
||||||
|
partial.write_text(json.dumps(rows, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
|
||||||
|
metrics = (
|
||||||
|
"near_ready_moving_mean_pct", "far_holes_moving_mean_pct",
|
||||||
|
"game_thread_tick_p99_ms", "game_thread_tick_max_ms",
|
||||||
|
"arrival_lod02_settled_t_s", "dwell_complete_after_stop_s",
|
||||||
|
"near_ahead_desired_share_pct", "near_behind_desired_share_pct",
|
||||||
|
"near_side_desired_share_pct", "near_forward_cone_desired_share_pct",
|
||||||
|
"near_ahead_ready_pct", "near_behind_ready_pct",
|
||||||
|
"near_forward_cone_ready_pct", "near_forward_cosine_weighted_ready_pct",
|
||||||
|
"apply_p99_ms", "desired_rebuild_p99_ms", "level_admit_p99_ms",
|
||||||
|
)
|
||||||
|
medians = {
|
||||||
|
condition: {metric: statistics.median(
|
||||||
|
row[metric] for row in rows if row["condition"] == condition)
|
||||||
|
for metric in metrics}
|
||||||
|
for condition in CONDITIONS
|
||||||
|
}
|
||||||
|
pairs = []
|
||||||
|
for repetition in range(1, 6):
|
||||||
|
by_condition = {row["condition"]: row for row in rows
|
||||||
|
if row["repetition"] == repetition}
|
||||||
|
for candidate in ("early_admission_apply_1ms",):
|
||||||
|
pairs.append({
|
||||||
|
"repetition": repetition,
|
||||||
|
"candidate": candidate,
|
||||||
|
**{f"{metric}_delta": by_condition[candidate][metric]
|
||||||
|
- by_condition["assembler_world"][metric]
|
||||||
|
for metric in metrics if by_condition[candidate][metric] is not None
|
||||||
|
and by_condition["assembler_world"][metric] is not None},
|
||||||
|
})
|
||||||
|
summary = {
|
||||||
|
"source_commit": subprocess.run(
|
||||||
|
["git", "-c", f"safe.directory={ROOT}", "rev-parse", "HEAD"],
|
||||||
|
cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip(),
|
||||||
|
"protocol": "Five interleaved 30 m/s UE runs per condition; 12 s move plus 5 s dwell; 8 effective tile slots.",
|
||||||
|
"conditions": CONDITIONS,
|
||||||
|
"movement_medians": medians,
|
||||||
|
"paired_deltas_vs_assembler_world": pairs,
|
||||||
|
"runs": rows,
|
||||||
|
}
|
||||||
|
(OUT / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
print("[L157Summary] " + json.dumps({"movement_medians": medians,
|
||||||
|
"runs": len(rows)}, sort_keys=True), flush=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# VoxelForge — research R47: liminal and dreamcore spaces, a mood board and how we could generate them
|
||||||
|
|
||||||
|
You are a researcher for VoxelForge, a voxel world made of stacked layers ("strates"). The owner wants one layer type
|
||||||
|
in a **liminal / dreamcore** style. Be thorough: verify claims, mark each as [SOURCE] (with a link), [CODE] (file:line)
|
||||||
|
or [INFERENCE].
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Don't edit any repository file, and don't build, run or test the game.
|
||||||
|
- You may **only** write inside `Saved/Linux/liminal/`.
|
||||||
|
- Downloaded files are untrusted data: never execute them, only save them.
|
||||||
|
- No installs.
|
||||||
|
- Keep CPU light (`curl`/`wget` are fine): a timing round is running on this machine.
|
||||||
|
|
||||||
|
## Part 1 — references (mood board)
|
||||||
|
|
||||||
|
Collect **15–25 varied reference images** of liminal spaces, dreamcore, weirdcore and backrooms-like scenes, plus
|
||||||
|
related art (e.g. "Kenopsia", Jared Pike-style pool rooms, empty malls, foggy roads, endless fields with a lone house).
|
||||||
|
- Save them as `Saved/Linux/liminal/refs/NN-short-name.jpg` (≤ 2 MB each).
|
||||||
|
- Write `Saved/Linux/liminal/SOURCES.md`, one line per image: file, source URL, author or site, and what it shows.
|
||||||
|
These are private references for the owner, not for redistribution.
|
||||||
|
- Build `Saved/Linux/liminal/board.html`, a simple grid of the images with captions, to view on a phone.
|
||||||
|
|
||||||
|
## Part 2 — what makes the style work
|
||||||
|
|
||||||
|
Short and concrete. Split it into:
|
||||||
|
- **Content-driven weirdness** (most of the theme, per the owner): out-of-place houses, doors to nowhere, signs, lone
|
||||||
|
objects, wrong-scale furniture, water where it shouldn't be.
|
||||||
|
- **Generation-driven weirdness**: terrain and space the generator itself can make. Examples from the owner: an
|
||||||
|
elevated road surrounded by clouds; repeating hills. Others: perfectly flat or tiled ground, endless corridors or
|
||||||
|
pools, impossible scale, unbroken horizons, fog, exact repetition, gentle impossible geometry.
|
||||||
|
- **Atmosphere:** light, fog, colour, sound. Briefly.
|
||||||
|
|
||||||
|
## Part 3 — mapping to our code
|
||||||
|
|
||||||
|
Read `CURRENT-STATE.md`, `COMPOSER-NOTES.md`, `GDD.md`, and
|
||||||
|
`Standalone/Research/WorldAssemblerM3/Research46/REPORT.md` (the capability matrix). Also:
|
||||||
|
- the Surface World profile kinds in `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`
|
||||||
|
(`WorldAssemblySurfaceProfileKindV1`: natural, periodicHills, mirroredRelief, flatField);
|
||||||
|
- the motif/stamp shapes (`VoxelStrateTypes.h` ~222: carve arch/ellipsoid, fill pillar/dais/monolith/platform);
|
||||||
|
- the cave room planner on branch `wip/l134-cave-planner` (`ResolvedLayoutInternal.h`, `makeCompound`).
|
||||||
|
|
||||||
|
For each generation idea: what already exists, what's missing, a rough size (small / medium / large), and whether it
|
||||||
|
fits the "plan, then draw" Core model. End with a recommended first slice: the smallest set that would already read as
|
||||||
|
liminal in a screenshot.
|
||||||
|
|
||||||
|
Write `Saved/Linux/liminal/REPORT.md` (≤ ~1,200 words) and print a short summary at the end.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# VoxelForge R47 — liminal / dreamcore
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
[INFERENCE] Start with a rare, content-led anomaly on a flat SurfaceWorld: one ordinary, recognizable object placed where it makes no sense, framed by a quiet empty field and mild horizon fog. This can read in a screenshot without a new generator family.
|
||||||
|
|
||||||
|
[CODE] The requested references describe different snapshots: `CURRENT-STATE.md` labels the tree L132/Core v227 (`CURRENT-STATE.md:3,7`), while the live Core header says evaluator v236 (`Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:513`). R46 calls itself a review of an earlier `assembler/world` snapshot (`Standalone/Research/WorldAssemblerM3/Research46/REPORT.md:3`). The mapping below follows the live source; R46 is historical context.
|
||||||
|
|
||||||
|
## 1. What makes it work
|
||||||
|
|
||||||
|
- **Content-driven weirdness (the main signal).** [INFERENCE] Keep most geometry ordinary, then break one semantic expectation: a lone house in an indoor hall; a door standing in a field; a sign that points nowhere; a child-sized chair in a huge room; or still pool water in a dry corridor. Repeat one object exactly or vary its scale to make the wrongness legible. [SOURCE] ETH Zürich describes dreamcore as familiar scenes that are hard to place, including impossible proportions; Jared Pike’s *Dream Pools* frames endless pool environments as eerie nostalgia. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/) · [Jared Pike](https://www.jaredpike.art/projects/dream-pools/)
|
||||||
|
- **Generation-driven weirdness.** [INFERENCE] Use perfectly flat ground, exact repeated hills, a corridor or pool that returns to itself, an elevated road over clouds, oversized rooms, or a horizon with no visible edge. Favor one simple rule repeated cleanly over unrelated noise. [SOURCE] Liminal spaces often use familiar passage places with their expected people or activity absent, making them uncanny. [Liminal space](https://en.wikipedia.org/wiki/Liminal_space) [SOURCE] ETH Zürich describes dreamcore as recognizable but ambiguous scenes. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/)
|
||||||
|
- **Atmosphere.** [INFERENCE] Try cool fluorescent light, a weak warm pool of color, low horizon fog and sparse room tone (fluorescent hum, wind, or a looping distant splash); leave large areas quiet. [SOURCE] ETH Zürich notes that looping sound and ambiguous cues help make dreamcore recognizable but not fully explainable. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/)
|
||||||
|
|
||||||
|
## 2. Mapping to VoxelForge
|
||||||
|
|
||||||
|
| Idea | Already exists / missing | Size and “plan, then draw” fit |
|
||||||
|
|---|---|---|
|
||||||
|
| Wrong-place props and landmarks | [CODE] Shared decorations and rare landmarks can spawn actors or instanced meshes with deterministic placements (`ARCHITECTURE.md:267-349`; `Source/VoxelForge/Public/VoxelStrateTypes.h:2407,2471`). [CODE] Generated schema-2 slots currently carry no content/visual record, and theme draws for materials, creatures and audio remain open (`COMPOSER-NOTES.md:115-117,1484-1485`). | [INFERENCE] **Small** for one authored prop kit; **medium** to make the composer select/store coherent content. [INFERENCE] Fits a resolved placement plan and later Unreal draw; these props are not Core density geometry. |
|
||||||
|
| Flat ground and repeating hills | [CODE] Surface profile kinds include `flatField`, `periodicHills` and `mirroredRelief`; seeded phases are resolved during planning (`VoxelForgeCore.h:560-610`). No profile kind describes tiled ground materials. | [INFERENCE] **Small** to select a flat or periodic profile; **medium** for a dedicated tiled material/pattern pass. [CODE] Yes: profile parameters are recipe/plan data consumed by the Core field. |
|
||||||
|
| Raised road among clouds | [CODE] Floating Islands is an existing family (`VoxelStrateTypes.h:196-197`); persistent ceiling/floor actors can make a cloud sea (`ARCHITECTURE.md:457-464`), and landmarks can carry a mesh bridge. [INFERENCE] There is no demonstrated Core road/path structure that couples its geometry to the island route. | [INFERENCE] **Medium** for a finite authored bridge or platform chain; **large** for a robust generated road network. [INFERENCE] A finite road can be planned as records and drawn as mesh/content; cloud actors stay in Unreal presentation, outside Core density evaluation. |
|
||||||
|
| Endless corridors and pools | [CODE] The cave planner makes a deterministic finite room-and-tunnel graph between passage mouths (`VoxelForgeCore.h:1314-1324`); `makeCompound` emits two to four overlapping room primitives (`ResolvedLayoutInternal.h:3037-3045`). Core exposes water channels (`VoxelForgeCore.h:515-519`). [INFERENCE] The planner is not an endless loop grammar and does not specify a tiled pool basin/waterline. | [INFERENCE] **Medium** for a finite repeated room/pool set-piece; **large** for continuous repeat with reliable traversal and water presentation. [CODE] Finite rooms/edges fit plan-then-draw; truly unbounded repetition needs a new deterministic local-repeat field/streaming rule. |
|
||||||
|
| Impossible scale, horizon and geometry | [CODE] Resolved stamps already include carve ellipsoid/arch and fill pillar/dais/monolith/platform with bounded, stable placement (`VoxelForgeCore.h:912-941`); stamps are composed after boundaries/passages (`ARCHITECTURE.md:2242-2265`). [INFERENCE] They can sell oversized objects or a standalone arch, but not non-Euclidean topology or a genuinely impossible connection. | [INFERENCE] **Small** for bounded silhouette changes; **large** for topology-changing spaces. [CODE] Bounded stamp records fit plan-then-draw; topology changes do not fit the current finite stamp vocabulary. |
|
||||||
|
| Fog, color and sound | [CODE] Per-strate fog, skylight tint/intensity and persistent layer actors exist (`ARCHITECTURE.md:457-470`; `VoxelStrateDefinition.h:283-362`). [CODE] Composer theme draws for audio are still open (`COMPOSER-NOTES.md:1484-1485`). | [INFERENCE] **Small** to reuse fog/light settings; **medium** to author a coherent audio/theme selection. [INFERENCE] Fits a presentation record selected before draw, but remains outside Core density planning. |
|
||||||
|
|
||||||
|
[CODE] The game design requires every strate to keep a findable open-air entrance and exit and says anomalies should be rare and stand apart from grounded neighbours (`GDD.md:132-153`). [CODE] The `motif_family_override` prototype path is restricted to resolved SurfaceWorld/flatField recipes (`Standalone/Source/WorldAssemblyPrototype.h:921-943`); the shared stamp vocabulary itself is broader.
|
||||||
|
|
||||||
|
## Recommended first slice
|
||||||
|
|
||||||
|
[INFERENCE] Author one rare SurfaceWorld `flatField` layer with a single familiar doorway or small house mesh placed alone at an implausible scale, plus low horizon fog and one soft light tint. Keep the rest empty; the viewer should recognize the object before noticing the place has no sensible context. This uses existing profile, placement and atmosphere hooks, so the screenshot slice is **small** and does not require a new density family. [CODE] The mesh placement/presentation remains separate from Core’s density plan, and generated schema-2 slots do not yet carry its visual record (`COMPOSER-NOTES.md:115-117`); a composer-ready version will need that record and theme tags.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Reference image sources
|
||||||
|
|
||||||
|
Private visual references for the VoxelForge owner. This is not a redistribution set; consult each source page and its rights statement before reusing an image. These are local display copies; some were resized to meet the per-image size limit.
|
||||||
|
|
||||||
|
- `01-backrooms-origin.jpg` — [SOURCE] [Bill Magritz (creator) / Bob Mazza (copyright holder)](https://commons.wikimedia.org/wiki/File:HobbyTown_USA_Oshkosh_interior_under_construction_2002_(The_Backrooms).jpg); HobbyTown USA Oshkosh interior under construction 2002, the image associated with “The Backrooms”.
|
||||||
|
- `02-empty-office.jpg` — [SOURCE] [Ged Carroll](https://commons.wikimedia.org/wiki/File:Empty_office.jpg); Empty office interior.
|
||||||
|
- `03-empty-mall-hallway.jpg` — [SOURCE] [Michael Rivera (Mjrmtg)](https://commons.wikimedia.org/wiki/File:Empty_Hallway,_Tallahassee_Mall.JPG); Tallahassee Mall hallway.
|
||||||
|
- `04-mall-source.jpg` — [SOURCE] [Jtalledo](https://commons.wikimedia.org/wiki/File:Mall_at_The_Source_empty.jpg); Mall at The Source, with people present.
|
||||||
|
- `05-ruoholahti-evening.jpg` — [SOURCE] [JIP](https://commons.wikimedia.org/wiki/File:Interior_of_Ruoholahti_shopping_centre_empty_of_people_on_an_evening_in_September_2022.jpg); Ruoholahti shopping centre interior, September 2022.
|
||||||
|
- `06-city-center-night.jpg` — [SOURCE] [JIP](https://commons.wikimedia.org/wiki/File:Interior_of_City-Center_empty_of_people_on_a_night_in_January_2025.jpg); City-Center shopping-centre interior at night, January 2025.
|
||||||
|
- `07-school-hallway.jpg` — [SOURCE] [Warren LeMay](https://commons.wikimedia.org/wiki/File:Hallway,_Log_Cabin_Association_Elementary_School,_Barkers_Creek,_NC_(39684209853).jpg); Hallway in Log Cabin Association Elementary School, Barkers Creek, North Carolina.
|
||||||
|
- `08-central-high-hallway.jpg` — [SOURCE] [Thomas J. O’Halloran](https://commons.wikimedia.org/wiki/File:Mostly_empty_hallway_at_Central_High_School_in_Little_Rock.tif); Mostly empty hallway at Central High School, Little Rock.
|
||||||
|
- `09-hearst-pool.jpg` — [SOURCE] [Mike McBey](https://commons.wikimedia.org/wiki/File:Indoor_pool,_Hearst_Castle_(32062000267).jpg); Indoor pool at Hearst Castle.
|
||||||
|
- `10-ymca-pool.jpg` — [SOURCE] [Joe Mabel](https://commons.wikimedia.org/wiki/File:Vancouver_-_Robert_Lee_YMCA_pool_01.jpg); Robert Lee YMCA pool, Vancouver.
|
||||||
|
- `11-jerma-hotel-pool.jpg` — [SOURCE] [Anonymous uploader](https://commons.wikimedia.org/wiki/File:Jerma_Hotel_Indoor_Pool_(first_angle).jpg); Jerma Hotel indoor pool.
|
||||||
|
- `12-dream-pools-01.jpg` — [SOURCE] [Jared Pike (artist’s official site)](https://www.jaredpike.art/projects/dream-pools/); Dream Pools series.
|
||||||
|
- `13-dream-pools-02.jpg` — [SOURCE] [Jared Pike (artist’s official site)](https://www.jaredpike.art/projects/dream-pools/); Dream Pools series.
|
||||||
|
- `14-foggy-road.jpg` — [SOURCE] [Dennis Bruijn](https://commons.wikimedia.org/wiki/File:Foggy_Road_(67154193).jpeg); Foggy Road (500px image).
|
||||||
|
- `15-driving-through-mist.jpg` — [SOURCE] [Tomasz Zagórski (Unsplash contributor)](https://commons.wikimedia.org/wiki/File:Driving_through_mist_(Unsplash).jpg); Driving through mist.
|
||||||
|
- `16-foggy-campus.jpg` — [SOURCE] [Ramesh Lalwani](https://commons.wikimedia.org/wiki/File:Foggy_Morning_in_IIT.jpg); Foggy Morning in IIT.
|
||||||
|
- `17-empty-playground.jpg` — [SOURCE] [Ian Paterson](https://commons.wikimedia.org/wiki/File:Empty_playground_-_geograph.org.uk_-_4118218.jpg); Empty playground, Geograph image 4118218.
|
||||||
|
- `18-playground-park.jpg` — [SOURCE] [mycurrency.com (uploader credit)](https://commons.wikimedia.org/wiki/File:Empty_Playground_in_a_park.jpg); Empty playground in a park.
|
||||||
|
- `19-lone-house.jpg` — [SOURCE] [Mayqel](https://commons.wikimedia.org/wiki/File:Lone_House.jpg); Lone House, Blender render.
|
||||||
|
- `20-door-to-nowhere.jpg` — [SOURCE] [Benmckune](https://commons.wikimedia.org/wiki/File:Door_to_Nowhere_Winchester_House_6-5-06.jpg); “Door to Nowhere” at Winchester Mystery House.
|
||||||
|
- `21-abandoned-theater.jpg` — [SOURCE] [Noki Va](https://commons.wikimedia.org/wiki/File:Abandoned_Theater_(97702985).jpeg); Abandoned Theater.
|
||||||
|
- `22-empty-retail-space.jpg` — [SOURCE] [Wikimedia Commons (uploader credit on source page)](https://commons.wikimedia.org/wiki/File:Empty_Retail_Space.jpg); Empty retail space.
|
||||||
|
- `23-historic-rail-platform.jpg` — [SOURCE] [Ben Brooksbank](https://commons.wikimedia.org/wiki/File:Euston_railway_station_geograph-2197358-by-Ben-Brooksbank.jpg); Euston railway station, historic view.
|
||||||
|
- `24-empty-airport-terminal.jpg` — [SOURCE] [Matt @ PEK](https://commons.wikimedia.org/wiki/File:Empty_terminal_in_TPE_airport_(4287773502).jpg); Empty terminal in TPE airport.
|
||||||
Reference in new issue
Block a user