fix(classifier): exact lattice check before a uniform verdict; ceiling cause is NOT false certificates
ParameterRoll's 5 false box verdicts are fixed: fine room-graph intervals are now checked against exact lattice samples before a uniform verdict is accepted, while the SDF bound keeps its conservative lower bound. Lattice sweep 600 tiles / 0 violations; ParameterRoll passes with 9,577 exact lattice leaves and 774 interval disagreements caught. The owner's ceiling holes are NOT explained by this: in his recorded session outer_classifier_mode=0, solid-proof skips were 0, and air-proof skips were outside the strate. The trace points at the far-field sheet system instead: sheet tile L5 (0,1,0) is deliberately empty inside the sheet hole while its bridge tile L4 (0,1,-1) meshed 662 ceiling triangles. Continuous ceiling is NOT verified: every render launch failed (D3D12 driver error, stalls before map load, the 6 GB cap), so the round produced no matched after-render. Cost note: this round used 3.46M tokens on Luna 6 at max, most of it burned on failed render launches. Verification by render is unreliable in this harness; verify ceiling continuity from tile/geometry data instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
30253f0e89
commit
ea3f0d9a52
7 files changed
+409
-17
No files matched your search
@@ -13,6 +13,7 @@
|
||||
#include "VoxelStrateComposer.h"
|
||||
#include "VoxelStrateMeasure.h"
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -238,8 +239,11 @@ namespace
|
||||
int32 AllAir = 0;
|
||||
int32 Proved = 0;
|
||||
int64 CheckedVoxels = 0;
|
||||
int64 ExactIntervalChecks = 0;
|
||||
int64 ExactIntervalDisagreements = 0;
|
||||
int32 Violations = 0;
|
||||
FString FirstViolation;
|
||||
TArray<FString> ViolationDetails;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -248,7 +252,8 @@ namespace
|
||||
* to expose. The report is aggregate: there is no per-sample logging.
|
||||
*/
|
||||
FBoxVerdictReport VF_CheckRolledBoxVerdicts(
|
||||
const VoxelForgeTest::FTestWorld& World, int32 CandidateIndex, int32 StrateIndex)
|
||||
const VoxelForgeTest::FTestWorld& World, int32 CandidateIndex, int32 StrateIndex,
|
||||
const FVoxelStrateRollInfo& Roll)
|
||||
{
|
||||
FBoxVerdictReport Report;
|
||||
const UVoxelGenerator* Generator = World.Generator.Get();
|
||||
@@ -286,8 +291,18 @@ namespace
|
||||
(float)(Origin.Y + (Cells + 1) * Step),
|
||||
(float)(Origin.Z + (Cells + 1) * Step)));
|
||||
(void)Box; // The production API receives the equivalent origin/step/cell box.
|
||||
const FBox ClassifierBox(
|
||||
FVector(static_cast<float>(Origin.X), static_cast<float>(Origin.Y),
|
||||
static_cast<float>(Origin.Z)),
|
||||
FVector(static_cast<float>(Origin.X + Cells * Step),
|
||||
static_cast<float>(Origin.Y + Cells * Step),
|
||||
static_cast<float>(Origin.Z + Cells * Step)));
|
||||
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(Origin, Step, Cells);
|
||||
FVoxelTileClassificationStats ClassifyStats;
|
||||
const EVoxelTileClass Verdict = Generator->ClassifyTile(
|
||||
Origin, Step, Cells, &ClassifyStats);
|
||||
Report.ExactIntervalChecks += ClassifyStats.ExactCoreIntervalChecks;
|
||||
Report.ExactIntervalDisagreements += ClassifyStats.ExactCoreIntervalDisagreements;
|
||||
if (Verdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
++Report.Mixed;
|
||||
@@ -305,6 +320,15 @@ namespace
|
||||
|
||||
const bool bClaimsSolid = Verdict == EVoxelTileClass::AllSolid;
|
||||
bool bBoxBad = false;
|
||||
bool bDiagnosticStackReady = false;
|
||||
bool bDiagnosticStackBuilt = false;
|
||||
FVoxelOpStack DiagnosticStack;
|
||||
FVoxelOpContext DiagnosticContext;
|
||||
EVoxelTileClass DiagnosticStackVerdict = EVoxelTileClass::Mixed;
|
||||
int32 DiagnosticSolidKiller = INDEX_NONE;
|
||||
int32 DiagnosticAirKiller = INDEX_NONE;
|
||||
TArray<FVoxelOpStack::FOpBoxDiagnostic> OpDiagnostics;
|
||||
FString WriterRouteDetails;
|
||||
// ClassifyTile covers the cell vertices passed to it: g is [0, Cells]. The mesher
|
||||
// samples a separate halo, but that halo is not part of this production verdict's
|
||||
// proof domain and must not turn a sound tile into a false failure.
|
||||
@@ -325,6 +349,173 @@ namespace
|
||||
{
|
||||
++Report.Violations;
|
||||
bBoxBad = true;
|
||||
if (!bDiagnosticStackReady)
|
||||
{
|
||||
bDiagnosticStackReady = true;
|
||||
int32 SlotTop = 0, SlotBottom = 0;
|
||||
FVoxelStrateArchetypeParams RuntimeParams = Roll.ArchetypeParams;
|
||||
bDiagnosticStackBuilt = World.GetSlotVoxelZRange(
|
||||
StrateIndex, SlotTop, SlotBottom);
|
||||
if (bDiagnosticStackBuilt)
|
||||
{
|
||||
VF_SetStrateArchetypeRuntimeBounds(
|
||||
RuntimeParams, static_cast<float>(SlotTop) + 1.0f,
|
||||
static_cast<float>(SlotBottom));
|
||||
bDiagnosticStackBuilt = VF_BuildNativeStrateStackForCandidate(
|
||||
Roll.Archetype, RuntimeParams, World.Settings->Seed,
|
||||
World.Generator->OriginSpineRadius,
|
||||
World.Generator->WorldRadiusVoxels,
|
||||
World.Generator->EdgeSealThickness,
|
||||
World.StrateManager.Get(), DiagnosticStack, DiagnosticContext);
|
||||
}
|
||||
if (bDiagnosticStackBuilt)
|
||||
{
|
||||
DiagnosticContext.ChunkCoord = FIntVector(
|
||||
0, 0, VF_FloorDiv(Origin.Z, CHUNK_SIZE));
|
||||
DiagnosticContext.Step = Step;
|
||||
DiagnosticContext.LayoutVersion =
|
||||
World.StrateManager->GetLayoutVersion();
|
||||
DiagnosticContext.WorldRadiusVoxels =
|
||||
World.Generator->WorldRadiusVoxels;
|
||||
DiagnosticContext.EdgeSealThickness =
|
||||
World.Generator->EdgeSealThickness;
|
||||
DiagnosticContext.bUseLatticeProof = true;
|
||||
DiagnosticContext.bTightenWarpProof = Step <= 2;
|
||||
DiagnosticContext.LatticeOriginVoxels = Origin;
|
||||
DiagnosticStack.PrepareChunk(DiagnosticContext);
|
||||
DiagnosticStackVerdict = DiagnosticStack.ClassifyBoxAttributed(
|
||||
ClassifierBox, DiagnosticContext,
|
||||
DiagnosticSolidKiller, DiagnosticAirKiller);
|
||||
DiagnosticStack.DiagnoseBox(
|
||||
ClassifierBox, DiagnosticContext, Step, OpDiagnostics,
|
||||
&DiagnosticStackVerdict);
|
||||
}
|
||||
}
|
||||
if (WriterRouteDetails.IsEmpty())
|
||||
{
|
||||
const FIntVector SampleChunk(
|
||||
VF_FloorDiv(static_cast<int32>(X), CHUNK_SIZE),
|
||||
VF_FloorDiv(static_cast<int32>(Y), CHUNK_SIZE),
|
||||
VF_FloorDiv(static_cast<int32>(Z), CHUNK_SIZE));
|
||||
int32 SampleSlotTop = 0;
|
||||
int32 SampleSlotBottom = 0;
|
||||
int32 RecipeSeedAtSample = 0;
|
||||
ECaveGeneratorType RecipeArchetypeAtSample = ECaveGeneratorType::TunnelNetwork;
|
||||
FVoxelStrateArchetypeParams RecipeParamsAtSample;
|
||||
FVoxelOpStackRecipe RecipeAtSample;
|
||||
const bool bSampleHasSlot = World.StrateManager->GetStrateChunkZBounds(
|
||||
SampleChunk.Z, SampleSlotTop, SampleSlotBottom);
|
||||
const bool bSampleHasRecipe = World.StrateManager->GetRecipeForChunk(
|
||||
SampleChunk, RecipeSeedAtSample, RecipeArchetypeAtSample,
|
||||
RecipeParamsAtSample, RecipeAtSample);
|
||||
const float PassageSdf = World.StrateManager->EvaluateModifierSDF(X, Y, Z);
|
||||
const float MaxPassageCarve =
|
||||
World.StrateManager->MaxPassageCarveFactorNearLattice(
|
||||
ClassifierBox, Origin, Step);
|
||||
const bool bPassageNear = World.StrateManager->AnyPassageNearLattice(
|
||||
ClassifierBox, Origin, Step);
|
||||
const bool bPassageAirPostNear = World.StrateManager->AnyPassageAirPostNearLattice(
|
||||
ClassifierBox, Origin, Step, 8.0f, World.Generator->EdgeSealThickness);
|
||||
const bool bLandingFloorNear =
|
||||
World.StrateManager->AnyPassageLandingFloorNearLattice(
|
||||
ClassifierBox, Origin, Step);
|
||||
const bool bStructuralPostNear =
|
||||
World.StrateManager->AnyPassageStructuralPostNearLattice(
|
||||
ClassifierBox, Origin, Step);
|
||||
WriterRouteDetails = FString::Printf(
|
||||
TEXT("sample_chunk=(%d,%d,%d) gap=%d slot_bounds=%d:[%d..%d] "
|
||||
"generator=%s operator_stack=%d recipe=%d world_radius=%.6g "
|
||||
"passage_sdf=%.9g passage_max_carve=%.9g "
|
||||
"passage_near=%d passage_air_post=%d landing_floor=%d structural_post=%d"),
|
||||
SampleChunk.X, SampleChunk.Y, SampleChunk.Z,
|
||||
World.StrateManager->IsGapChunk(SampleChunk) ? 1 : 0,
|
||||
bSampleHasSlot ? 1 : 0, SampleSlotTop, SampleSlotBottom,
|
||||
VF_GetStrateArchetypeName(
|
||||
World.StrateManager->GetGeneratorTypeForChunk(SampleChunk)),
|
||||
World.StrateManager->UsesOperatorStackForChunk(SampleChunk) ? 1 : 0,
|
||||
bSampleHasRecipe ? 1 : 0, World.Generator->WorldRadiusVoxels,
|
||||
PassageSdf, MaxPassageCarve, bPassageNear ? 1 : 0,
|
||||
bPassageAirPostNear ? 1 : 0, bLandingFloorNear ? 1 : 0,
|
||||
bStructuralPostNear ? 1 : 0);
|
||||
}
|
||||
float StackDensity = FLT_MAX;
|
||||
FString StackAttribution = TEXT("stack diagnostic unavailable");
|
||||
if (bDiagnosticStackBuilt)
|
||||
{
|
||||
StackDensity = DiagnosticStack.EvalMC(X, Y, Z);
|
||||
FString BoundViolations;
|
||||
FString SdfIntervalViolations;
|
||||
FString OperatorIntervals;
|
||||
for (const FVoxelOpStack::FOpBoxDiagnostic& OpDiagnostic : OpDiagnostics)
|
||||
{
|
||||
if (OpDiagnostic.ForcedVerdict != EVoxelTileClass::Mixed
|
||||
|| OpDiagnostic.bHasDensityDelta
|
||||
|| OpDiagnostic.bHasSdfValue)
|
||||
{
|
||||
if (!OperatorIntervals.IsEmpty()) { OperatorIntervals += TEXT("; "); }
|
||||
OperatorIntervals += FString::Printf(
|
||||
TEXT("%s(forced=%d,effect=%d,carve=%.6g,fill=%.6g,delta=[%.6g,%.6g],sdf_bound=[%.6g,%.6g],sdf_actual=[%.6g,%.6g])"),
|
||||
*OpDiagnostic.Name,
|
||||
static_cast<int32>(OpDiagnostic.ForcedVerdict),
|
||||
static_cast<int32>(OpDiagnostic.Effect),
|
||||
OpDiagnostic.MaxCarve, OpDiagnostic.MaxFill,
|
||||
OpDiagnostic.ActualDensityDeltaMin,
|
||||
OpDiagnostic.ActualDensityDeltaMax,
|
||||
OpDiagnostic.SdfBoundMin, OpDiagnostic.SdfBoundMax,
|
||||
OpDiagnostic.ActualSdfMin, OpDiagnostic.ActualSdfMax);
|
||||
}
|
||||
if (OpDiagnostic.bCarveBoundViolated
|
||||
|| OpDiagnostic.bFillBoundViolated)
|
||||
{
|
||||
if (!BoundViolations.IsEmpty()) { BoundViolations += TEXT(", "); }
|
||||
BoundViolations += FString::Printf(
|
||||
TEXT("%s(carve=%d,fill=%d,delta=[%.6g,%.6g])"),
|
||||
*OpDiagnostic.Name,
|
||||
OpDiagnostic.bCarveBoundViolated ? 1 : 0,
|
||||
OpDiagnostic.bFillBoundViolated ? 1 : 0,
|
||||
OpDiagnostic.ActualDensityDeltaMin,
|
||||
OpDiagnostic.ActualDensityDeltaMax);
|
||||
}
|
||||
if (OpDiagnostic.bHasSdfBound && OpDiagnostic.bHasSdfValue
|
||||
&& (OpDiagnostic.ActualSdfMin < OpDiagnostic.SdfBoundMin - 1.0e-4f
|
||||
|| OpDiagnostic.ActualSdfMax > OpDiagnostic.SdfBoundMax + 1.0e-4f))
|
||||
{
|
||||
if (!SdfIntervalViolations.IsEmpty())
|
||||
{
|
||||
SdfIntervalViolations += TEXT(", ");
|
||||
}
|
||||
SdfIntervalViolations += FString::Printf(
|
||||
TEXT("%s(bound=[%.6g,%.6g],actual=[%.6g,%.6g])"),
|
||||
*OpDiagnostic.Name, OpDiagnostic.SdfBoundMin,
|
||||
OpDiagnostic.SdfBoundMax, OpDiagnostic.ActualSdfMin,
|
||||
OpDiagnostic.ActualSdfMax);
|
||||
}
|
||||
}
|
||||
StackAttribution = FString::Printf(
|
||||
TEXT("stack_verdict=%s solid_killer=%s air_killer=%s stack_density=%.9g "
|
||||
"operator_bound_violations=[%s] sdf_interval_violations=[%s] "
|
||||
"op_intervals=[%s] "
|
||||
"refine_nodes=%u depth=%u exact_core_samples=%u exact_final_samples=%u "
|
||||
"post_nodes=%u"),
|
||||
DiagnosticStackVerdict == EVoxelTileClass::AllSolid ? TEXT("AllSolid")
|
||||
: DiagnosticStackVerdict == EVoxelTileClass::AllAir ? TEXT("AllAir")
|
||||
: TEXT("Mixed"),
|
||||
DiagnosticStack.GetOpDebugName(DiagnosticSolidKiller),
|
||||
DiagnosticStack.GetOpDebugName(DiagnosticAirKiller),
|
||||
StackDensity,
|
||||
BoundViolations.IsEmpty() ? TEXT("none") : *BoundViolations,
|
||||
SdfIntervalViolations.IsEmpty() ? TEXT("none") : *SdfIntervalViolations,
|
||||
OperatorIntervals.IsEmpty() ? TEXT("none") : *OperatorIntervals,
|
||||
ClassifyStats.RefineNodes, ClassifyStats.MaxRefinementDepth,
|
||||
ClassifyStats.ExactCoreSamples, ClassifyStats.ExactFinalSamples,
|
||||
ClassifyStats.NeedsFinalFieldNodes);
|
||||
}
|
||||
Report.ViolationDetails.Add(FString::Printf(
|
||||
TEXT("candidate %d box %d origin (%d,%d,%d) said %s; final density at "
|
||||
"(%0.f,%0.f,%0.f) = %.9g; %s; %s"),
|
||||
CandidateIndex, BoxIndex, Origin.X, Origin.Y, Origin.Z,
|
||||
bClaimsSolid ? TEXT("AllSolid") : TEXT("AllAir"),
|
||||
X, Y, Z, Density, *WriterRouteDetails, *StackAttribution));
|
||||
if (Report.FirstViolation.IsEmpty())
|
||||
{
|
||||
Report.FirstViolation = FString::Printf(
|
||||
@@ -801,6 +992,8 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
int32 TotalBoxAllAir = 0;
|
||||
int32 TotalBoxProved = 0;
|
||||
int64 TotalBoxCheckedVoxels = 0;
|
||||
int64 TotalExactIntervalChecks = 0;
|
||||
int64 TotalExactIntervalDisagreements = 0;
|
||||
int32 TotalBoxViolations = 0;
|
||||
int32 NumTunnelLikeCandidates = 0;
|
||||
int32 NumCandidatesWithLiveDetail = 0;
|
||||
@@ -996,16 +1189,23 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
|
||||
const FBoxVerdictReport BoxReport = VF_CheckRolledBoxVerdicts(
|
||||
World, CandidateIndex, CandidateStrateIndex);
|
||||
World, CandidateIndex, CandidateStrateIndex, Roll);
|
||||
TotalBoxMixed += BoxReport.Mixed;
|
||||
TotalBoxAllSolid += BoxReport.AllSolid;
|
||||
TotalBoxAllAir += BoxReport.AllAir;
|
||||
TotalBoxProved += BoxReport.Proved;
|
||||
TotalBoxCheckedVoxels += BoxReport.CheckedVoxels;
|
||||
TotalExactIntervalChecks += BoxReport.ExactIntervalChecks;
|
||||
TotalExactIntervalDisagreements += BoxReport.ExactIntervalDisagreements;
|
||||
TotalBoxViolations += BoxReport.Violations;
|
||||
if (BoxReport.Violations > 0)
|
||||
{
|
||||
BoxViolationCandidates.Add(CandidateIndex);
|
||||
for (const FString& ViolationDetail : BoxReport.ViolationDetails)
|
||||
{
|
||||
AddInfo(FString::Printf(TEXT("ROLLED BOX-VERDICT COUNTEREXAMPLE: %s"),
|
||||
*ViolationDetail));
|
||||
}
|
||||
if (FirstBoxViolation.IsEmpty())
|
||||
{
|
||||
FirstBoxViolation = BoxReport.FirstViolation;
|
||||
@@ -1098,6 +1298,10 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
|
||||
TotalBoxMixed, TotalBoxAllSolid, TotalBoxAllAir, TotalBoxProved,
|
||||
TotalBoxCheckedVoxels, TotalBoxViolations,
|
||||
BoxViolationCandidates.Num() == 0 ? TEXT("none") : TEXT("see first violation below")));
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Fine room-graph interval checks: %lld exact lattice leaves; %lld interval verdicts "
|
||||
"disagreed with the exact child samples."),
|
||||
TotalExactIntervalChecks, TotalExactIntervalDisagreements));
|
||||
if (!BoxViolationCandidates.IsEmpty())
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
|
||||
@@ -6753,7 +6753,12 @@ namespace
|
||||
FVoxelBoxSdfInterval Own;
|
||||
if (bUseExactLatticeWarp)
|
||||
{
|
||||
Own.Set(ExactSdfLower, FLT_MAX);
|
||||
// Exact per-primitive scans are a tightening only. Their candidate-domain
|
||||
// filters can miss a warped lattice query near a primitive, so retain the
|
||||
// broad lower bound over the AABB that contains every warped sample. The
|
||||
// minimum stays useful when both agree and remains safe when an exact scan is
|
||||
// incomplete.
|
||||
Own.Set(FMath::Min(ExactSdfLower, CheapSdfLower), FLT_MAX);
|
||||
}
|
||||
else if (!bAnyPrimitive)
|
||||
{
|
||||
|
||||
@@ -6328,6 +6328,34 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack,
|
||||
static_cast<float>(Box.Max.Z - Box.Min.Z));
|
||||
const bool bSmallFinalFieldBox = bNeedsFinalField
|
||||
&& FinalFieldBoxExtent <= 8.0f;
|
||||
|
||||
// A room-graph SDF interval can be conservative at the parent while a fine child still
|
||||
// resolves uniformly through a state-aware SDF writer (for example the worm mask). Before
|
||||
// using that fine interval to skip geometry, certify the exact MC lattice in the child.
|
||||
// This runs only after a mixed parent has been refined to a small room-graph child; clear
|
||||
// uniform roots and all non-room stacks keep the interval-only fast path.
|
||||
const bool bNeedsExactRoomIntervalCheck = bHasRoomGeometry
|
||||
&& Context.bUseLatticeProof && Context.bTightenWarpProof
|
||||
&& !bNeedsFinalField && Whole != EVoxelTileClass::Mixed
|
||||
&& Depth >= 3 && FinalFieldBoxExtent <= 8.0f;
|
||||
if (bNeedsExactRoomIntervalCheck)
|
||||
{
|
||||
int32 ExactIX0 = 0, ExactIY0 = 0, ExactIZ0 = 0;
|
||||
int32 ExactIX1 = 0, ExactIY1 = 0, ExactIZ1 = 0;
|
||||
int64 ExactCount = 0;
|
||||
if (GetLatticeBounds(Box, ExactIX0, ExactIY0, ExactIZ0,
|
||||
ExactIX1, ExactIY1, ExactIZ1, ExactCount)
|
||||
&& ExactCount <= 8192)
|
||||
{
|
||||
if (Stats) { ++Stats->ExactCoreIntervalChecks; }
|
||||
const EVoxelTileClass Exact = ClassifyExactLatticeLeaf(Box, false);
|
||||
if (Exact != Whole && Stats)
|
||||
{
|
||||
++Stats->ExactCoreIntervalDisagreements;
|
||||
}
|
||||
return Exact;
|
||||
}
|
||||
}
|
||||
if (bSmallFinalFieldBox)
|
||||
{
|
||||
int32 ExactIX0 = 0, ExactIY0 = 0, ExactIZ0 = 0;
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
#include "Materials/MaterialParameterCollection.h"
|
||||
#include "Kismet/KismetMaterialLibrary.h"
|
||||
#include "Engine/VolumeTexture.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Camera/CameraActor.h"
|
||||
#include "VoxelAtmosphereManager.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "IImageWrapper.h"
|
||||
@@ -1533,6 +1535,55 @@ void AVoxelWorld::BeginPlay()
|
||||
return;
|
||||
}
|
||||
|
||||
int32 TestRenderDistanceChunks = 0;
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.TestRenderDistanceChunks="),
|
||||
TestRenderDistanceChunks)
|
||||
&& TestRenderDistanceChunks > 0)
|
||||
{
|
||||
if (UVoxelSettings* TestSettings = DuplicateObject<UVoxelSettings>(Settings, this))
|
||||
{
|
||||
const int32 AuthoredRenderDistance = Settings->RenderDistanceChunks;
|
||||
TestSettings->RenderDistanceChunks = TestRenderDistanceChunks;
|
||||
Settings = TestSettings;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTestCeiling] transient_render_distance_chunks=%d authored=%d"),
|
||||
TestRenderDistanceChunks, AuthoredRenderDistance);
|
||||
}
|
||||
}
|
||||
|
||||
int32 TestCeilingView = 0;
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingView="), TestCeilingView);
|
||||
bTestCeilingViewEnabled = TestCeilingView != 0;
|
||||
if (bTestCeilingViewEnabled)
|
||||
{
|
||||
TestCeilingCameraWorldPosition = FVector(10583.003176, 20477.525456, -38442.958976);
|
||||
TestCeilingCameraRotation = FRotator(78.399901, 255.200004, 0.0);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraX="),
|
||||
TestCeilingCameraWorldPosition.X);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraY="),
|
||||
TestCeilingCameraWorldPosition.Y);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraZ="),
|
||||
TestCeilingCameraWorldPosition.Z);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraPitch="),
|
||||
TestCeilingCameraRotation.Pitch);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCameraYaw="),
|
||||
TestCeilingCameraRotation.Yaw);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestCeilingCaptureDelaySeconds="),
|
||||
TestCeilingCaptureDelaySeconds);
|
||||
TestCeilingCaptureDelaySeconds = FMath::Max(TestCeilingCaptureDelaySeconds, 1.0f);
|
||||
TestCeilingViewBeginSeconds = FPlatformTime::Seconds();
|
||||
bHeadlessStreamingTestCenterOverride = true;
|
||||
HeadlessStreamingTestCenterVoxel = WorldToLocalVoxel(TestCeilingCameraWorldPosition);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTestCeiling] camera_world_cm=(%.3f,%.3f,%.3f) "
|
||||
"camera_rotation=(%.3f,%.3f,%.3f) center_vox=(%.3f,%.3f,%.3f) capture_delay_s=%.1f"),
|
||||
TestCeilingCameraWorldPosition.X, TestCeilingCameraWorldPosition.Y,
|
||||
TestCeilingCameraWorldPosition.Z, TestCeilingCameraRotation.Pitch,
|
||||
TestCeilingCameraRotation.Yaw, TestCeilingCameraRotation.Roll,
|
||||
HeadlessStreamingTestCenterVoxel.X, HeadlessStreamingTestCenterVoxel.Y,
|
||||
HeadlessStreamingTestCenterVoxel.Z, TestCeilingCaptureDelaySeconds);
|
||||
}
|
||||
|
||||
// The owner's current DA_Settings is intentionally a one-strate surface layout, so it cannot
|
||||
// exercise the separate inter-strate traversal regression. The explicit headless crossing
|
||||
// switch uses a transient copy of the authored four-definition corpus; no package is modified
|
||||
@@ -3667,6 +3718,45 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
ProcessUnloadQueue();
|
||||
MaybeFinishHeadlessStreamingTest();
|
||||
|
||||
if (bTestCeilingViewEnabled)
|
||||
{
|
||||
UWorld* World = GetWorld();
|
||||
APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr;
|
||||
if (PC && !TestCeilingCamera.IsValid())
|
||||
{
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
ACameraActor* Camera = World->SpawnActor<ACameraActor>(
|
||||
ACameraActor::StaticClass(), TestCeilingCameraWorldPosition,
|
||||
TestCeilingCameraRotation, SpawnParams);
|
||||
if (IsValid(Camera))
|
||||
{
|
||||
TestCeilingCamera = Camera;
|
||||
PC->SetViewTarget(Camera);
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTestCeiling] camera_installed=1 applied_visible_tiles=%llu"),
|
||||
static_cast<unsigned long long>(AppliedVisibleTileCount));
|
||||
}
|
||||
}
|
||||
|
||||
if (TestCeilingCamera.IsValid() && !bTestCeilingCaptureRequested
|
||||
&& AppliedVisibleTileCount >= 16
|
||||
&& FPlatformTime::Seconds() - TestCeilingViewBeginSeconds
|
||||
>= static_cast<double>(TestCeilingCaptureDelaySeconds))
|
||||
{
|
||||
bTestCeilingCaptureRequested = true;
|
||||
const bool bExecSucceeded = GEngine
|
||||
&& GEngine->Exec(World, TEXT("HighResShot 1"));
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTestCeiling] capture_requested=%d elapsed_s=%.3f "
|
||||
"visible_tiles=%llu triangles=%llu"),
|
||||
bExecSucceeded ? 1 : 0,
|
||||
FPlatformTime::Seconds() - TestCeilingViewBeginSeconds,
|
||||
static_cast<unsigned long long>(AppliedVisibleTileCount),
|
||||
static_cast<unsigned long long>(AppliedTriangleCount));
|
||||
}
|
||||
}
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
// Density-volume overlay (step 1a): cyan boxes for solid level-0 cells near the player.
|
||||
if (DensityVolume) { DensityVolume->DebugDraw(); }
|
||||
@@ -6068,7 +6158,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
/ static_cast<double>(WormEligibleSamples)
|
||||
: 0.0;
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d triangles=%d "
|
||||
TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d triangles=%d ceiling_triangles=%d "
|
||||
"band=(%d,%d) "
|
||||
"verdict=%d proof=%d air_proof=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f "
|
||||
"cache_build=%.6f evaluation=%.6f "
|
||||
@@ -6089,7 +6179,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
"worm_skip_rate=%.6f"),
|
||||
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells,
|
||||
bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
|
||||
Result.bEmpty ? 1 : 0, Result.NumTriangles, BandChunkLo, BandChunkHi,
|
||||
Result.bEmpty ? 1 : 0, Result.NumTriangles, Result.NumCeilingTriangles,
|
||||
BandChunkLo, BandChunkHi,
|
||||
ClassifyVerdict, Result.bSealedSolidProof ? 1 : 0,
|
||||
Result.bOutOfLayoutAirProof ? 1 : 0,
|
||||
ClassifySeconds, MeshSeconds, StreamSeconds,
|
||||
@@ -6428,6 +6519,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
// AND cap in one tile — entirely with the winner's material.)
|
||||
const int32 NumTris = MeshData.Triangles.Num() / 3;
|
||||
Result.NumTriangles = NumTris;
|
||||
Result.NumCeilingTriangles = MeshData.NumCeilingTriangles;
|
||||
Result.bHasCeilingTris = MeshData.NumCeilingTriangles > 0;
|
||||
Result.bHasGroundTris = NumTris > MeshData.NumCeilingTriangles;
|
||||
}
|
||||
|
||||
@@ -504,6 +504,8 @@ struct FVoxelTileClassificationStats
|
||||
uint32 MaxRefinementDepth = 0;
|
||||
uint32 ExactCoreSamples = 0;
|
||||
uint32 ExactFinalSamples = 0;
|
||||
uint32 ExactCoreIntervalChecks = 0;
|
||||
uint32 ExactCoreIntervalDisagreements = 0;
|
||||
uint32 ExactCoreCacheHits = 0;
|
||||
uint32 ExactFinalCacheHits = 0;
|
||||
uint32 ExactCoreLeaves = 0;
|
||||
|
||||
@@ -32,6 +32,7 @@ class UVolumeTexture;
|
||||
class UMaterialInterface;
|
||||
class UMaterialInstanceDynamic;
|
||||
class FScopedGenerationPause;
|
||||
class ACameraActor;
|
||||
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
|
||||
|
||||
/** Per-request cancellation state. The game thread flips the flag when a pending tile leaves the
|
||||
@@ -107,7 +108,7 @@ struct FChunkResult
|
||||
uint64 GenerationStartCycles = 0;
|
||||
uint64 GenerationEndCycles = 0;
|
||||
uint64 ApplyStartCycles = 0;
|
||||
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0;
|
||||
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0;
|
||||
bool bSealedSolidProof = false; // true when the cheap gap/seal proof skipped meshing
|
||||
bool bOutOfLayoutAirProof = false; // true when the exact constant-air guard skipped meshing
|
||||
// Clean, low-overhead work counters. These are carried with the result so the worker can
|
||||
@@ -1011,6 +1012,16 @@ public:
|
||||
double HeadlessStreamingTestBeginSeconds = 0.0;
|
||||
double HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
||||
|
||||
// Transient owner-view render hook. Enabled only by explicit voxel.TestCeilingView arguments;
|
||||
// the camera and smaller test radius are held in memory and never saved to a package.
|
||||
bool bTestCeilingViewEnabled = false;
|
||||
bool bTestCeilingCaptureRequested = false;
|
||||
TWeakObjectPtr<ACameraActor> TestCeilingCamera;
|
||||
FVector TestCeilingCameraWorldPosition = FVector::ZeroVector;
|
||||
FRotator TestCeilingCameraRotation = FRotator::ZeroRotator;
|
||||
double TestCeilingViewBeginSeconds = 0.0;
|
||||
float TestCeilingCaptureDelaySeconds = 18.0f;
|
||||
|
||||
// Explicit high-speed collision-gate repro. The streamer stays centred at the launch tile while
|
||||
// the character uses ordinary swept movement across the still-pending desired set. The test
|
||||
// distinguishes a gate hold from a full horizontal stop so a fall-only gate can be validated.
|
||||
|
||||
+60
-10
@@ -28,6 +28,7 @@ $EngineRoot = 'E:\Program Files\Epic Games\UE_5.7'
|
||||
$EditorExe = Join-Path $EngineRoot 'Engine\Binaries\Win64\UnrealEditor-Cmd.exe'
|
||||
$UbtDll = Join-Path $EngineRoot 'Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll'
|
||||
$LaunchTimeoutSeconds = 1800
|
||||
$UnrealPrivateMemoryLimitBytes = 6GB
|
||||
# The VoxelForge category includes two late, several-minute op-stack sweeps. They remain in
|
||||
# the suite; when the serialized aggregate launch reaches this 30-minute guard, validate those
|
||||
# named tests with -TestFilter so every test still runs under the standing launch limit.
|
||||
@@ -264,17 +265,48 @@ function Get-NewCrashes($Before, [datetime]$StartedUtc) {
|
||||
return @($new)
|
||||
}
|
||||
|
||||
function Wait-OwnedProcess($Process, [int]$TimeoutSeconds) {
|
||||
$finished = $Process.WaitForExit($TimeoutSeconds * 1000)
|
||||
if ($finished) {
|
||||
function Wait-OwnedProcess($Process, [int]$TimeoutSeconds, [switch]$EnforceMemoryLimit) {
|
||||
$watch = [Diagnostics.Stopwatch]::StartNew()
|
||||
$peakPrivateBytes = 0L
|
||||
while ($watch.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
|
||||
if ($Process.HasExited) {
|
||||
[void]$Process.WaitForExit()
|
||||
return [ordered]@{ timed_out = $false; exit_code = [int]$Process.ExitCode }
|
||||
return [ordered]@{
|
||||
timed_out = $false
|
||||
memory_limit_hit = $false
|
||||
peak_private_bytes = $peakPrivateBytes
|
||||
exit_code = [int]$Process.ExitCode
|
||||
}
|
||||
}
|
||||
if ($EnforceMemoryLimit) {
|
||||
$owned = Get-Process -Id ([int]$Process.Id) -ErrorAction SilentlyContinue
|
||||
if ($null -ne $owned) {
|
||||
$peakPrivateBytes = [Math]::Max($peakPrivateBytes, [int64]$owned.PrivateMemorySize64)
|
||||
if ([int64]$owned.PrivateMemorySize64 -gt $UnrealPrivateMemoryLimitBytes) {
|
||||
# This PID came from our Start-Process call. Do not use a name-wide kill here.
|
||||
try { Stop-Process -Id ([int]$Process.Id) -Force -ErrorAction Stop } catch { }
|
||||
try { [void]$Process.WaitForExit(5000) } catch { }
|
||||
return [ordered]@{
|
||||
timed_out = $false
|
||||
memory_limit_hit = $true
|
||||
peak_private_bytes = $peakPrivateBytes
|
||||
exit_code = $null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Start-Sleep -Milliseconds 1000
|
||||
}
|
||||
# This PID came from our Start-Process call. Never use a name-based kill or a broad process
|
||||
# query here: an owner's editor may be unrelated and must remain untouched.
|
||||
try { Stop-Process -Id ([int]$Process.Id) -Force -ErrorAction Stop } catch { }
|
||||
try { [void]$Process.WaitForExit(5000) } catch { }
|
||||
return [ordered]@{ timed_out = $true; exit_code = $null }
|
||||
return [ordered]@{
|
||||
timed_out = $true
|
||||
memory_limit_hit = $false
|
||||
peak_private_bytes = $peakPrivateBytes
|
||||
exit_code = $null
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPath,
|
||||
@@ -293,6 +325,8 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
|
||||
ended_utc = $null
|
||||
exit_code = $null
|
||||
timed_out = $false
|
||||
memory_limit_hit = $false
|
||||
peak_private_bytes = 0L
|
||||
crash_reporters_reaped = @()
|
||||
new_crashes = @()
|
||||
log = $LogPath
|
||||
@@ -309,12 +343,22 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
|
||||
# -LocalDataCachePath are supplied. Keep this child process inside the run directory.
|
||||
$env:LOCALAPPDATA = $isolatedLocalAppData
|
||||
$process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot `
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle Hidden -PassThru
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle Hidden `
|
||||
-PassThru
|
||||
try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal }
|
||||
catch {
|
||||
try { Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop } catch { }
|
||||
throw "Could not lower owned Unreal PID $($process.Id) to BelowNormal: $($_.Exception.Message)"
|
||||
}
|
||||
$ledger.pid = [int]$process.Id
|
||||
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds
|
||||
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds -EnforceMemoryLimit
|
||||
$ledger.timed_out = [bool]$wait.timed_out
|
||||
$ledger.memory_limit_hit = [bool]$wait.memory_limit_hit
|
||||
$ledger.peak_private_bytes = [int64]$wait.peak_private_bytes
|
||||
$ledger.exit_code = $wait.exit_code
|
||||
$ledger.status = if ($ledger.timed_out) { 'timeout' } elseif ($ledger.exit_code -eq 0) { 'passed' } else { 'failed' }
|
||||
$ledger.status = if ($ledger.timed_out) { 'timeout' }
|
||||
elseif ($ledger.memory_limit_hit) { 'memory_limit' }
|
||||
elseif ($ledger.exit_code -eq 0) { 'passed' } else { 'failed' }
|
||||
} catch {
|
||||
$ledger.status = 'launch_error'
|
||||
$ledger.error = $_.Exception.Message
|
||||
@@ -356,7 +400,13 @@ function Invoke-BuildProcess([string[]]$Arguments, [string]$LogPath, [string]$Er
|
||||
$env:DOTNET_NOLOGO = '1'
|
||||
$env:LOCALAPPDATA = $isolatedLocalAppData
|
||||
$process = Start-Process -FilePath $dotnet -ArgumentList $Arguments -WorkingDirectory $PluginRoot `
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $ErrorPath -WindowStyle Hidden -PassThru
|
||||
-RedirectStandardOutput $StdoutPath -RedirectStandardError $ErrorPath -WindowStyle Hidden `
|
||||
-PassThru
|
||||
try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal }
|
||||
catch {
|
||||
try { Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop } catch { }
|
||||
throw "Could not lower owned build PID $($process.Id) to BelowNormal: $($_.Exception.Message)"
|
||||
}
|
||||
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds
|
||||
} finally {
|
||||
if ($null -eq $oldDotnetCliHome) { Remove-Item Env:DOTNET_CLI_HOME -ErrorAction SilentlyContinue } else { $env:DOTNET_CLI_HOME = $oldDotnetCliHome }
|
||||
@@ -894,7 +944,7 @@ try {
|
||||
$buildStdout = Join-Path $RunRoot 'Logs\build.stdout.log'
|
||||
$buildError = Join-Path $RunRoot 'Logs\build.err.log'
|
||||
$buildArgs = @(('"{0}"' -f $UbtDll), 'UnrealEditor', 'Win64', 'Development', ('-Project="{0}"' -f $HostProject),
|
||||
'-WaitMutex', '-FromMsBuild', '-architecture=x64', '-NoUBA', '-MaxParallelActions=4',
|
||||
'-WaitMutex', '-FromMsBuild', '-architecture=x64', '-NoUBA', '-MaxParallelActions=2',
|
||||
('-Log="{0}"' -f $buildLog))
|
||||
$BuildRecord = Invoke-BuildProcess $buildArgs $buildLog $buildError $buildStdout
|
||||
if ($BuildRecord.status -ne 'passed') { Add-Failure "staged build $($BuildRecord.status)" }
|
||||
|
||||
Reference in new issue
Block a user