From ea3f0d9a5264a184157044d71e27a9a3012ca322 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Wed, 23 Sep 2026 06:30:05 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- .../VoxelForgeComposerParameterRollTest.cpp | 210 +++++++++++++++++- .../Private/VoxelDensityOpStack.cpp | 7 +- Source/VoxelForge/Private/VoxelGenerator.cpp | 28 +++ Source/VoxelForge/Private/VoxelWorld.cpp | 96 +++++++- Source/VoxelForge/Public/VoxelGenerator.h | 2 + Source/VoxelForge/Public/VoxelWorld.h | 13 +- Tools/VoxelForgeTest.ps1 | 72 +++++- 7 files changed, 410 insertions(+), 18 deletions(-) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerParameterRollTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerParameterRollTest.cpp index 5b48da7..e1c42b5 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerParameterRollTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerParameterRollTest.cpp @@ -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 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(Origin.X), static_cast(Origin.Y), + static_cast(Origin.Z)), + FVector(static_cast(Origin.X + Cells * Step), + static_cast(Origin.Y + Cells * Step), + static_cast(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 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(SlotTop) + 1.0f, + static_cast(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(X), CHUNK_SIZE), + VF_FloorDiv(static_cast(Y), CHUNK_SIZE), + VF_FloorDiv(static_cast(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(OpDiagnostic.ForcedVerdict), + static_cast(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( diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 64d5b6c..beb53e3 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -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) { diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 0a3d986..05485ea 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -6328,6 +6328,34 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack, static_cast(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; diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index d230762..0a60633 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -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(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::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(AppliedVisibleTileCount)); + } + } + + if (TestCeilingCamera.IsValid() && !bTestCeilingCaptureRequested + && AppliedVisibleTileCount >= 16 + && FPlatformTime::Seconds() - TestCeilingViewBeginSeconds + >= static_cast(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(AppliedVisibleTileCount), + static_cast(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(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; } diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index eb724e9..8e49d68 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -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; diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index 2d8d532..6a7c0ff 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -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 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. diff --git a/Tools/VoxelForgeTest.ps1 b/Tools/VoxelForgeTest.ps1 index 61e6324..07c5c0c 100644 --- a/Tools/VoxelForgeTest.ps1 +++ b/Tools/VoxelForgeTest.ps1 @@ -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) { - [void]$Process.WaitForExit() - return [ordered]@{ timed_out = $false; exit_code = [int]$Process.ExitCode } +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 + 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)" }