diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 10c3aea..4179e66 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -1333,6 +1333,11 @@ VoxelDensityProfile::FCacheMemoryBreakdown FChunkSDFCache::GetAllocatedSizeBreak Breakdown.TunnelsBytes += ArrayBytes(Tunnel.WorldFloorProfiles); } + for (const FCachedRoom& Room : Rooms) + { + Breakdown.RoomsBytes += ArrayBytes(Room.MouthRises); + } + return Breakdown; } @@ -1356,6 +1361,15 @@ struct FBuildRoom bool bPlayerFitAttempted = false; }; +struct FBuildRoomMouthRise +{ + int32 RoomIndex = INDEX_NONE; + FVector Mouth = FVector::ZeroVector; + float TargetFloorZ = 0.0f; + float BlendRadius = 0.0f; + float Strength = 0.0f; +}; + struct FVFRoomGraphReach { float DirectRoomReach = 0.0f; @@ -1395,6 +1409,8 @@ static bool VF_ComputeRoomGraphReach( Params.RoomFloorCutMax, Params.FloorReliefStrength, Params.FloorReliefFrequency, + Params.RoomMouthRiseStrength, + Params.RoomMouthRiseBlendVoxels, Params.OriginRoomRadius, Params.TunnelMinRadius, Params.TunnelMaxRadius, @@ -5112,6 +5128,7 @@ void VoxelCaveMorphology::BuildChunkCache( // set (and each candidate's own candidates) lies inside the COLLECT region. TArray> RoomConnected; RoomConnected.Init(false, NumRooms); + TArray> RoomMouthRises; const auto ResolvePlayerFitPoint = [](FBuildRoom& Room, const FStrateGenerationParams& InParams, uint32 InWorldSeed) -> bool @@ -5426,6 +5443,59 @@ void VoxelCaveMorphology::BuildChunkCache( - FMath::Abs(CT.WorldControlRadii.Last()); } + // Author optional room-side benches now that the tunnel mouth floors are known. A + // rise is admitted only toward a higher mouth and only when the room's own ceiling + // still contains the player capsule. The evaluator later reads this descriptor after + // the room's base floor is resolved; no per-sample tunnel search or correction is + // involved. Strength zero is the exact legacy path. + const float MouthRiseStrength = FMath::Clamp( + FMath::IsFinite(Params.RoomMouthRiseStrength) + ? Params.RoomMouthRiseStrength : 0.0f, + 0.0f, 1.0f); + const float MouthRiseBlend = FMath::Clamp( + FMath::IsFinite(Params.RoomMouthRiseBlendVoxels) + ? Params.RoomMouthRiseBlendVoxels : 8.0f, + 8.0f, 128.0f); + const float RequiredRoomClearance = + VoxelPassageGeometry::PlayerHeightVoxels + + VoxelPassageGeometry::HeadroomVoxels; + const auto AddRoomMouthRise = [ + &RoomMouthRises, &RoomFloorZFor, MouthRiseStrength, + MouthRiseBlend, RequiredRoomClearance]( + int32 RoomIndex, const FBuildRoom& Room, + const FVector& Mouth, float TargetFloorZ) + { + if (!(MouthRiseStrength > 0.0f) + || !FMath::IsFinite(TargetFloorZ) + || !FMath::IsFinite(Mouth.X) + || !FMath::IsFinite(Mouth.Y) + || !FMath::IsFinite(Mouth.Z)) + { + return; + } + const float BaseFloorZ = RoomFloorZFor(Room); + if (!FMath::IsFinite(BaseFloorZ) + || TargetFloorZ <= BaseFloorZ + KINDA_SMALL_NUMBER + || Room.Center.Z + Room.RadiusZ - TargetFloorZ + < RequiredRoomClearance) + { + return; + } + FBuildRoomMouthRise& Rise = RoomMouthRises.Emplace_GetRef(); + Rise.RoomIndex = RoomIndex; + Rise.Mouth = Mouth; + Rise.TargetFloorZ = TargetFloorZ; + Rise.BlendRadius = MouthRiseBlend; + Rise.Strength = MouthRiseStrength; + }; + if (CT.ControlPoints.Num() >= 2) + { + AddRoomMouthRise( + I, RoomA, CT.ControlPoints[0], CT.SDFMouthFloorZA); + AddRoomMouthRise( + J, RoomB, CT.ControlPoints.Last(), CT.SDFMouthFloorZB); + } + CT.Midpoint = CT.ControlPoints.Num() > 2 ? CT.ControlPoints[CT.ControlPoints.Num() / 2] : FVector::ZeroVector; @@ -5838,6 +5908,28 @@ void VoxelCaveMorphology::BuildChunkCache( } } + // Attach the complete room-side mouth descriptors after the room shape/floor fields are + // baked. Expand the conservative room cull to cover the local blend even when the mouth + // is offset from the room centre. + for (const FBuildRoomMouthRise& AuthoredRise : RoomMouthRises) + { + if (AuthoredRise.RoomIndex != RoomIdx) + { + continue; + } + FCachedRoomMouthRise& Rise = CR.MouthRises.Emplace_GetRef(); + Rise.Mouth = AuthoredRise.Mouth; + Rise.TargetFloorZ = AuthoredRise.TargetFloorZ; + Rise.BlendRadius = AuthoredRise.BlendRadius; + Rise.Strength = AuthoredRise.Strength; + const float Reach = static_cast(FVector::Dist( + CR.Center, Rise.Mouth)) + FMath::Max(Rise.BlendRadius, 0.0f); + if (FMath::IsFinite(Reach)) + { + CR.CullRadiusSq = FMath::Max(CR.CullRadiusSq, Reach * Reach); + } + } + OutCache.Rooms.Add(CR); //--------------------------------------------------------------------- @@ -5982,6 +6074,27 @@ float VoxelCaveMorphology::EvaluateSDFCached( if (Room.FloorCutZ > -FLT_MAX) { float FloorZ = Room.FloorCutZ; + for (const FCachedRoomMouthRise& Rise : Room.MouthRises) + { + const float DX = static_cast(Pos.X - Rise.Mouth.X); + const float DY = static_cast(Pos.Y - Rise.Mouth.Y); + const float Radius = FMath::Max(Rise.BlendRadius, 1.0f); + const float Distance = FMath::Sqrt(DX * DX + DY * DY); + if (Distance >= Radius + || !(Rise.Strength > 0.0f) + || !FMath::IsFinite(Rise.TargetFloorZ)) + { + continue; + } + const float Weight = 1.0f - SmoothStep01( + FMath::Clamp(Distance / Radius, 0.0f, 1.0f)); + const float DesiredFloorZ = FMath::Lerp( + FloorZ, Rise.TargetFloorZ, + Weight * FMath::Clamp(Rise.Strength, 0.0f, 1.0f)); + // A lower tunnel never cuts down into the room. The room remains the owner of + // its floor, while a higher mouth receives a deliberate, smooth bench. + FloorZ = FMath::Max(FloorZ, DesiredFloorZ); + } const float FloorBlend = BlendK * 0.35f; bool bFloorCutIsIdentity = false; if (Room.FloorReliefStrength > 0.0f) diff --git a/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Source/VoxelForge/Private/VoxelDensityProfile.cpp index 666de63..142b90d 100644 --- a/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -432,6 +432,13 @@ namespace VoxelDensityProfile case ECounter::MesherBlockTests: return TEXT("MesherBlockTests"); case ECounter::MesherBlockAllSolid:return TEXT("MesherBlockAllSolid"); case ECounter::MesherBlockAllAir: return TEXT("MesherBlockAllAir"); + case ECounter::TileClassifyCalls: return TEXT("TileClassifyCalls"); + case ECounter::TileClassifyAllSolid: return TEXT("TileClassifyAllSolid"); + case ECounter::TileClassifyAllAir: return TEXT("TileClassifyAllAir"); + case ECounter::TileClassifyMixed: return TEXT("TileClassifyMixed"); + case ECounter::TileVerdictCacheHits: return TEXT("TileVerdictCacheHits"); + case ECounter::TileVerdictRegionHits: return TEXT("TileVerdictRegionHits"); + case ECounter::TileVerdictCacheStores: return TEXT("TileVerdictCacheStores"); case ECounter::Count: break; } return TEXT("Unknown"); diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index c5e102e..93aafed 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -20,6 +20,7 @@ #include "VoxelDensityProfile.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "HAL/PlatformTime.h" +#include "Misc/ScopeLock.h" #if WITH_EDITOR #include "VoxelStrateComposer.h" @@ -1314,6 +1315,169 @@ UVoxelGenerator::UVoxelGenerator() { } +void UVoxelGenerator::InvalidateTileVerdictCache() const +{ + FScopeLock Lock(&TileVerdictCacheLock); + TileVerdictCache.Reset(); + TileVerdictCacheClock = 0; +} + +bool UVoxelGenerator::FindTileVerdict( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, + int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion, + uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites, + EVoxelTileClass& OutVerdict, bool& bOutRegionHit) const +{ + bOutRegionHit = false; + FScopeLock Lock(&TileVerdictCacheLock); + ++TileVerdictCacheClock; + + const int64 RequestMaxX = static_cast(OriginVoxels.X) + + static_cast(CellsPerAxis) * Step; + const int64 RequestMaxY = static_cast(OriginVoxels.Y) + + static_cast(CellsPerAxis) * Step; + const int64 RequestMaxZ = static_cast(OriginVoxels.Z) + + static_cast(CellsPerAxis) * Step; + + int32 BestIndex = INDEX_NONE; + bool bBestExact = false; + for (int32 Index = 0; Index < TileVerdictCache.Num(); ++Index) + { + FTileVerdictCacheEntry& Entry = TileVerdictCache[Index]; + if (Entry.GeneratorSeed != Seed + || Entry.ManagerWorldSeed != ManagerWorldSeed + || Entry.OriginSpineRadius != OriginSpineRadius + || Entry.WorldRadiusVoxels != WorldRadiusVoxels + || Entry.EdgeSealThickness != EdgeSealThickness + || Entry.ManagerLifetimeId != (StrateManager ? StrateManager->GetCacheLifetimeId() : 0) + || Entry.LayoutVersion != LayoutVersion + || Entry.DiffVersion != DiffVersion + || Entry.ParamsFingerprint != ParamsFingerprint + || Entry.PassageSupportFloorWrites != PassageSupportFloorWrites + || Entry.Step != Step + || Entry.CellsPerAxis < CellsPerAxis) + { + continue; + } + + const int64 EntryMaxX = static_cast(Entry.Origin.X) + + static_cast(Entry.CellsPerAxis) * Step; + const int64 EntryMaxY = static_cast(Entry.Origin.Y) + + static_cast(Entry.CellsPerAxis) * Step; + const int64 EntryMaxZ = static_cast(Entry.Origin.Z) + + static_cast(Entry.CellsPerAxis) * Step; + const int64 DeltaX = static_cast(OriginVoxels.X) - Entry.Origin.X; + const int64 DeltaY = static_cast(OriginVoxels.Y) - Entry.Origin.Y; + const int64 DeltaZ = static_cast(OriginVoxels.Z) - Entry.Origin.Z; + if (DeltaX < 0 || DeltaY < 0 || DeltaZ < 0 + || (DeltaX % Step) != 0 || (DeltaY % Step) != 0 || (DeltaZ % Step) != 0 + || RequestMaxX > EntryMaxX || RequestMaxY > EntryMaxY || RequestMaxZ > EntryMaxZ) + { + continue; + } + + const bool bExact = Entry.Origin == OriginVoxels + && Entry.CellsPerAxis == CellsPerAxis; + if (BestIndex == INDEX_NONE || (bExact && !bBestExact) + || (bExact == bBestExact + && Entry.CellsPerAxis < TileVerdictCache[BestIndex].CellsPerAxis)) + { + BestIndex = Index; + bBestExact = bExact; + } + } + + if (BestIndex == INDEX_NONE) + { + return false; + } + + FTileVerdictCacheEntry& Entry = TileVerdictCache[BestIndex]; + Entry.LastUse = TileVerdictCacheClock; + OutVerdict = Entry.Verdict; + bOutRegionHit = !(Entry.Origin == OriginVoxels + && Entry.CellsPerAxis == CellsPerAxis); + return OutVerdict != EVoxelTileClass::Mixed; +} + +void UVoxelGenerator::StoreTileVerdict( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, + int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion, + uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites, + EVoxelTileClass Verdict) const +{ + if (Verdict == EVoxelTileClass::Mixed) + { + return; + } + + FScopeLock Lock(&TileVerdictCacheLock); + ++TileVerdictCacheClock; + + int32 SlotIndex = INDEX_NONE; + int32 VictimIndex = INDEX_NONE; + uint32 OldestUse = MAX_uint32; + for (int32 Index = 0; Index < TileVerdictCache.Num(); ++Index) + { + FTileVerdictCacheEntry& Entry = TileVerdictCache[Index]; + if (Entry.GeneratorSeed == Seed + && Entry.ManagerWorldSeed == ManagerWorldSeed + && Entry.OriginSpineRadius == OriginSpineRadius + && Entry.WorldRadiusVoxels == WorldRadiusVoxels + && Entry.EdgeSealThickness == EdgeSealThickness + && Entry.ManagerLifetimeId == (StrateManager ? StrateManager->GetCacheLifetimeId() : 0) + && Entry.LayoutVersion == LayoutVersion + && Entry.DiffVersion == DiffVersion + && Entry.ParamsFingerprint == ParamsFingerprint + && Entry.PassageSupportFloorWrites == PassageSupportFloorWrites + && Entry.Origin == OriginVoxels + && Entry.Step == Step + && Entry.CellsPerAxis == CellsPerAxis) + { + SlotIndex = Index; + break; + } + if (Entry.LastUse < OldestUse) + { + OldestUse = Entry.LastUse; + VictimIndex = Index; + } + } + + if (SlotIndex == INDEX_NONE) + { + if (TileVerdictCache.Num() < TileVerdictCacheCapacity) + { + SlotIndex = TileVerdictCache.AddDefaulted(); + } + else if (VictimIndex != INDEX_NONE) + { + SlotIndex = VictimIndex; + } + else + { + return; + } + } + + FTileVerdictCacheEntry& Entry = TileVerdictCache[SlotIndex]; + Entry.Origin = OriginVoxels; + Entry.Step = Step; + Entry.CellsPerAxis = CellsPerAxis; + Entry.GeneratorSeed = Seed; + Entry.ManagerWorldSeed = ManagerWorldSeed; + Entry.OriginSpineRadius = OriginSpineRadius; + Entry.WorldRadiusVoxels = WorldRadiusVoxels; + Entry.EdgeSealThickness = EdgeSealThickness; + Entry.ManagerLifetimeId = StrateManager ? StrateManager->GetCacheLifetimeId() : 0; + Entry.LayoutVersion = LayoutVersion; + Entry.DiffVersion = DiffVersion; + Entry.ParamsFingerprint = ParamsFingerprint; + Entry.PassageSupportFloorWrites = PassageSupportFloorWrites; + Entry.Verdict = Verdict; + Entry.LastUse = TileVerdictCacheClock; +} + void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings) { // Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas @@ -1322,6 +1486,7 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings) OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 14.0f; WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f; EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 64.0f; + InvalidateTileVerdictCache(); } float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const @@ -4571,6 +4736,92 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, FVoxelTileClassificationStats* OutStats) const +{ + if (OutStats) + { + *OutStats = FVoxelTileClassificationStats(); + } + + const int32 NormalizedStep = FMath::Max(1, Step); + const int32 NormalizedCellsPerAxis = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE); + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileClassifyCalls); + + const uint64 ParamsFingerprint = StrateManager + ? StrateManager->GetGenerationParamsFingerprint() : 0; + const bool bCacheable = ParamsFingerprint != 0; + const uint32 LayoutVersion = StrateManager ? StrateManager->GetLayoutVersion() : 0; + const uint32 DiffVersion = DiffLayer ? DiffLayer->GetModsVersion() : 0; + const int32 ManagerWorldSeed = StrateManager ? StrateManager->GetWorldSeed() : 0; + const uint8 PassageSupportFloorWrites = StrateManager + && StrateManager->ArePassageSupportFloorWritesEnabledForDiagnostics() ? 1u : 0u; + + EVoxelTileClass CachedVerdict = EVoxelTileClass::Mixed; + bool bRegionHit = false; + if (bCacheable + && FindTileVerdict( + OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, + ManagerWorldSeed, LayoutVersion, DiffVersion, ParamsFingerprint, + PassageSupportFloorWrites, CachedVerdict, bRegionHit)) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileVerdictCacheHits); + if (bRegionHit) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileVerdictRegionHits); + } + if (CachedVerdict == EVoxelTileClass::AllSolid) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileClassifyAllSolid); + } + else if (CachedVerdict == EVoxelTileClass::AllAir) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileClassifyAllAir); + } + else + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileClassifyMixed); + } + return CachedVerdict; + } + + const EVoxelTileClass Verdict = ClassifyTileUncached( + OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, OutStats); + if (Verdict == EVoxelTileClass::AllSolid) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileClassifyAllSolid); + } + else if (Verdict == EVoxelTileClass::AllAir) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileClassifyAllAir); + } + else + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileClassifyMixed); + } + + if (bCacheable && Verdict != EVoxelTileClass::Mixed) + { + StoreTileVerdict( + OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, + ManagerWorldSeed, LayoutVersion, DiffVersion, ParamsFingerprint, + PassageSupportFloorWrites, Verdict); + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::TileVerdictCacheStores); + } + return Verdict; +} + +EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, + FVoxelTileClassificationStats* OutStats) const { if (OutStats) { *OutStats = FVoxelTileClassificationStats(); } // Mêmes clamps que GenerateMesh — le verdict doit couvrir le treillis réellement échantillonné. diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index badff5f..c7d4ba3 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -11,6 +11,7 @@ #include "VoxelDensityProfile.h" // Opt-in targeted per-voxel attribution #include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo #include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten) +#include "Misc/Crc.h" #include "UObject/UObjectGlobals.h" #include @@ -289,7 +290,8 @@ static bool VF_IsAnyPassageFloorAt( if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) - || VF_IsWalkableTunnelFloor(Passage, Position))) + || (Manager->ArePassageSupportFloorWritesEnabledForDiagnostics() + && VF_IsWalkableTunnelFloor(Passage, Position)))) { return true; } @@ -1778,7 +1780,8 @@ void UVoxelStrateManager::ApplyPassageModifier( if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) - || VF_IsWalkableTunnelFloor(Passage, Position))) + || (bPassageSupportFloorWritesEnabled + && VF_IsWalkableTunnelFloor(Passage, Position)))) { // This is the one bidirectional part of PassageCarveOp: a floor is a proved solid // support slab. It is deliberately applied after the air carve so a tube can never @@ -1916,7 +1919,8 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC( if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) - || VF_IsWalkableTunnelFloor(Passage, Position))) + || (bPassageSupportFloorWritesEnabled + && VF_IsWalkableTunnelFloor(Passage, Position)))) { // Result is in MC convention here (negative = solid). This reassertion is the // structural floor backstop after the optional MC-space disturbance layer. @@ -1978,7 +1982,8 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC( if (!bSuppressFloor && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) - || VF_IsWalkableTunnelFloor(Passage, Position))) + || (bPassageSupportFloorWritesEnabled + && VF_IsWalkableTunnelFloor(Passage, Position)))) { bAnyPassageFloor = true; } @@ -4878,3 +4883,78 @@ FStrateGenerationParams UVoxelStrateManager::BuildParamsFromDefinition(const UVo // Base params only — terrain op fields stay 0 until per-room assignment. return Definition->GenerationParams; } + +uint64 UVoxelStrateManager::GetGenerationParamsFingerprint() const +{ + // The session cache is deliberately conservative. These inputs are valid density inputs but + // are not represented by the fixed-size hash below, so returning zero disables verdict reuse + // instead of pretending that a partial key is complete. + if (IsUsingSeason()) + { + return 0; + } +#if WITH_EDITOR + if (ComposerOverrides.Num() > 0) + { + return 0; + } +#endif + + uint32 A = 0x9E3779B9u; + uint32 B = 0x85EBCA6Bu; + auto HashBytes = [&A, &B](const void* Data, int32 Size) + { + A = FCrc::MemCrc32(Data, Size, A); + B = FCrc::MemCrc32(Data, Size, B ^ 0xA511E9B3u); + }; + auto HashValue = [&HashBytes](const auto& Value) + { + HashBytes(&Value, sizeof(Value)); + }; + + HashValue(CachedSeed); + HashValue(bOpenSurfaceEntry); + HashValue(OriginSpineRadius); + HashValue(InterStrateGapChunks); + const int32 LayoutCount = StrateLayout.Num(); + HashValue(LayoutCount); + + for (const FStrateSlot& Slot : StrateLayout) + { + if (Slot.Definition == nullptr) + { + return 0; + } + + // BuildChunkCache resolves these arrays into the room cache. Hashing UObject pointers or + // asset names would not prove the asset contents, so dynamic operation/biome layouts + // conservatively opt out. The normal synthetic/runtime profile has neither array. + if (Slot.Definition->TerrainOperations.Num() > 0 + || Slot.Definition->Biomes.Num() > 0) + { + return 0; + } + + HashValue(Slot.StrateIndex); + HashValue(Slot.TopChunkZ); + HashValue(Slot.BottomChunkZ); + HashValue(Slot.HeightInChunks); + HashValue(Slot.Definition->GeneratorType); + HashValue(Slot.Definition->bUseOperatorStack); + HashValue(Slot.Definition->StrateHeightInChunks); + HashValue(Slot.Definition->TransitionType); + HashValue(Slot.Definition->TransitionBlendChunks); + HashValue(Slot.Definition->GenerationParams); + HashValue(Slot.Definition->SlabParams); + HashValue(Slot.Definition->MazeParams); + HashValue(Slot.Definition->SurfaceParams); + HashValue(Slot.Definition->VerticalShaftParams); + HashValue(Slot.Definition->FloatingIslandParams); + HashValue(Slot.Definition->PassageConfig); + HashValue(Slot.Definition->bHasWater); + HashValue(Slot.Definition->BiomeMapParams); + } + + const uint64 Result = (static_cast(B) << 32) | static_cast(A); + return Result != 0 ? Result : 1; +} diff --git a/Source/VoxelForge/Public/VoxelCaveMorphology.h b/Source/VoxelForge/Public/VoxelCaveMorphology.h index baef9b2..cf5770b 100644 --- a/Source/VoxelForge/Public/VoxelCaveMorphology.h +++ b/Source/VoxelForge/Public/VoxelCaveMorphology.h @@ -556,6 +556,16 @@ struct FCachedPinch float BaseDensity = 0.0f; }; +// A build-time room-floor bench authored from one tunnel mouth. The room evaluator only reads +// these immutable descriptors; it never searches the graph or decides a correction per sample. +struct FCachedRoomMouthRise +{ + FVector Mouth = FVector::ZeroVector; + float TargetFloorZ = 0.0f; + float BlendRadius = 8.0f; + float Strength = 0.0f; +}; + // A room in the cache — everything needed for per-voxel SDF evaluation. // Internal details (CellX, CellY) used during cache building are NOT stored here. struct FCachedRoom @@ -579,6 +589,10 @@ struct FCachedRoom float FloorReliefFrequency; uint32 FloorSeed; // Deterministic seed offset for this room's floor noise + // Optional build-time floor benches leading to higher tunnel mouths. Empty in the default + // compatibility profile, so the baseline field is unchanged until an archetype opts in. + TArray MouthRises; + // Per-room terrain operation, hash-rolled during BuildChunkCache from the // strate's probability pool (FStrateTerrainOpEntry::Probability). // null = this room has no terrain op (the "no op" slot in the probability pool). diff --git a/Source/VoxelForge/Public/VoxelDensityProfile.h b/Source/VoxelForge/Public/VoxelDensityProfile.h index 4fa7d8c..6622f04 100644 --- a/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -114,6 +114,13 @@ namespace VoxelDensityProfile MesherBlockTests, MesherBlockAllSolid, MesherBlockAllAir, + TileClassifyCalls, + TileClassifyAllSolid, + TileClassifyAllAir, + TileClassifyMixed, + TileVerdictCacheHits, + TileVerdictRegionHits, + TileVerdictCacheStores, Count }; diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index e27be55..bfb35e6 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -9,6 +9,7 @@ #pragma once #include "CoreMinimal.h" +#include "HAL/CriticalSection.h" #include "VoxelTypes.h" #include "VoxelStrateTypes.h" #include "VoxelBiomeTypes.h" @@ -114,8 +115,16 @@ public: UPROPERTY() const UVoxelDiffLayer* DiffLayer = nullptr; - void SetStrateManager(const UVoxelStrateManager* InManager) { StrateManager = InManager; } - void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer) { DiffLayer = InDiffLayer; } + void SetStrateManager(const UVoxelStrateManager* InManager) + { + StrateManager = InManager; + InvalidateTileVerdictCache(); + } + void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer) + { + DiffLayer = InDiffLayer; + InvalidateTileVerdictCache(); + } // Copie le seed depuis les settings. Appelé au démarrage et lors d'un ChangeSeed. void InitializeSettings(const UVoxelSettings* Settings); @@ -351,6 +360,57 @@ private: * Process-unique owner identity for the `CP_*` thread-local cache key. */ uint64 DensityCacheOwnerId = 0; + /** + * A bounded, session-resident proof cache for uniform MC tiles. + * + * This is deliberately not a disk cache: a verdict is only useful while the exact generator + * and its immutable layout are alive, and a stale "all air" verdict would be a hole. The key + * carries every runtime invalidator that can change the sampled field. Entries are only + * stored for AllSolid/AllAir; Mixed is never remembered because it is not a proof. + * + * A larger proven tile is also a proof for an aligned sub-tile. FindTileVerdict therefore + * accepts an enclosing entry and records that separately from an exact revisit. Capacity is + * fixed so repeated exploration cannot grow memory without bound. + */ + struct FTileVerdictCacheEntry + { + FIntVector Origin = FIntVector::ZeroValue; + int32 Step = 1; + int32 CellsPerAxis = 0; + int32 GeneratorSeed = 0; + int32 ManagerWorldSeed = 0; + float OriginSpineRadius = 0.0f; + float WorldRadiusVoxels = 0.0f; + float EdgeSealThickness = 0.0f; + uint64 ManagerLifetimeId = 0; + uint32 LayoutVersion = 0; + uint32 DiffVersion = 0; + uint64 ParamsFingerprint = 0; + uint8 PassageSupportFloorWrites = 1; + EVoxelTileClass Verdict = EVoxelTileClass::Mixed; + uint32 LastUse = 0; + }; + + static constexpr int32 TileVerdictCacheCapacity = 512; + mutable FCriticalSection TileVerdictCacheLock; + mutable TArray TileVerdictCache; + mutable uint32 TileVerdictCacheClock = 0; + + void InvalidateTileVerdictCache() const; + bool FindTileVerdict( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, + int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion, + uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites, + EVoxelTileClass& OutVerdict, bool& bOutRegionHit) const; + void StoreTileVerdict( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, + int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion, + uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites, + EVoxelTileClass Verdict) const; + EVoxelTileClass ClassifyTileUncached( + const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis, + struct FVoxelTileClassificationStats* OutStats) const; + /** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */ int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const; diff --git a/Source/VoxelForge/Public/VoxelStrateManager.h b/Source/VoxelForge/Public/VoxelStrateManager.h index aa8b135..746aa29 100644 --- a/Source/VoxelForge/Public/VoxelStrateManager.h +++ b/Source/VoxelForge/Public/VoxelStrateManager.h @@ -240,6 +240,26 @@ public: * live edit) can never serve a stale strate index or passage shortlist. */ uint32 GetLayoutVersion() const { return PassagesVersion; } + /** + * Fingerprint of the immutable density inputs used by the generator. + * + * Returns zero when the manager has dynamic inputs that this compact cache key intentionally + * does not serialize (seasons, composer recipes, biome maps, or terrain-op assets). Zero is a + * fail-closed answer: callers must skip verdict reuse rather than risk an old all-air proof. + */ + uint64 GetGenerationParamsFingerprint() const; + + /** Diagnostic-only switch for isolating the inter-strate support writer. */ + void SetPassageSupportFloorWritesEnabledForDiagnostics(bool bEnabled) + { + bPassageSupportFloorWritesEnabled = bEnabled; + } + + bool ArePassageSupportFloorWritesEnabledForDiagnostics() const + { + return bPassageSupportFloorWritesEnabled; + } + /** * Get the strate definition for a specific chunk coordinate. * @@ -499,6 +519,9 @@ protected: // VoxelSettings::InterStrateGapChunks during Initialize(). int32 InterStrateGapChunks = 0; + // Production is true. This diagnostic switch never changes the passage carve or air contract. + bool bPassageSupportFloorWritesEnabled = true; + // Per-passage tunnel shape now lives on each UVoxelStrateDefinition::PassageConfig // (the upper strate of each boundary controls its own descent tunnels). diff --git a/Source/VoxelForge/Public/VoxelStrateTypes.h b/Source/VoxelForge/Public/VoxelStrateTypes.h index 727568b..a4c606a 100644 --- a/Source/VoxelForge/Public/VoxelStrateTypes.h +++ b/Source/VoxelForge/Public/VoxelStrateTypes.h @@ -272,6 +272,8 @@ enum class EVoxelStrateTransition : uint8 LERPF(RoomFloorCutMax) \ LERPF(FloorReliefStrength) \ LERPF(FloorReliefFrequency) \ + LERPF(RoomMouthRiseStrength) \ + LERPF(RoomMouthRiseBlendVoxels) \ LERPF(OriginRoomRadius) \ SNAPF(OriginRoomMaxConnections) \ /* Cave morphology — tunnels */ \ @@ -559,6 +561,17 @@ struct VOXELFORGE_API FStrateGenerationParams meta = (ClampMin = "0.001", EditCondition = "RoomFloorCutMin < 1.0")) float FloorReliefFrequency = 0.015f; + // Optional local room-floor bench that rises toward a higher tunnel mouth. It is authored + // from the build-time tunnel descriptor, not discovered by a post-generation validator. + // Strength 0 keeps the legacy room/tunnel hand-off; 1 reaches the compatible mouth floor. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms|Mouth Blend", + meta = (ClampMin = "0.0", ClampMax = "1.0")) + float RoomMouthRiseStrength = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Cave Morphology|Rooms|Mouth Blend", + meta = (ClampMin = "8.0", ClampMax = "128.0")) + float RoomMouthRiseBlendVoxels = 8.0f; + // How much variety in room shapes (0 = all ellipsoids, 1 = full variety). // At 0: every room is a smooth ellipsoid (uniform, organic caves). // At 0.5: some rooms become angular (rounded boxes) or elongated (capsules). diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 083d9c0..5bb90e5 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -147,6 +147,69 @@ bool ParseArchetype(const FString& Text, ECaveGeneratorType& OutArchetype) return false; } +const TCHAR* NoiseTypeName(EVoxelNoiseType NoiseType) +{ + switch (NoiseType) + { + case EVoxelNoiseType::FBM: return TEXT("FBM"); + case EVoxelNoiseType::Ridged: return TEXT("Ridged"); + case EVoxelNoiseType::Mixed: return TEXT("Mixed"); + case EVoxelNoiseType::Cellular: return TEXT("Cellular"); + default: return TEXT("Unknown"); + } +} + +bool ParseNoiseType(const FString& Text, EVoxelNoiseType& OutNoiseType) +{ + FString Normalized = Text; + Normalized.TrimStartAndEndInline(); + Normalized.ToLowerInline(); + Normalized.ReplaceInline(TEXT("_"), TEXT("")); + Normalized.ReplaceInline(TEXT("-"), TEXT("")); + + if (Normalized == TEXT("fbm") || Normalized == TEXT("fractal")) + { + OutNoiseType = EVoxelNoiseType::FBM; + return true; + } + if (Normalized == TEXT("ridged") || Normalized == TEXT("ridge")) + { + OutNoiseType = EVoxelNoiseType::Ridged; + return true; + } + if (Normalized == TEXT("mixed")) + { + OutNoiseType = EVoxelNoiseType::Mixed; + return true; + } + if (Normalized == TEXT("cellular") || Normalized == TEXT("worley")) + { + OutNoiseType = EVoxelNoiseType::Cellular; + return true; + } + return false; +} + +bool ParseRenderCamera(const FString& Text, FVector& OutCamera) +{ + TArray Components; + Text.ParseIntoArray(Components, TEXT(","), true); + if (Components.Num() != 3) + { + return false; + } + + const float X = FCString::Atof(*Components[0]); + const float Y = FCString::Atof(*Components[1]); + const float Z = FCString::Atof(*Components[2]); + if (!FMath::IsFinite(X) || !FMath::IsFinite(Y) || !FMath::IsFinite(Z)) + { + return false; + } + OutCamera = FVector(X, Y, Z); + return true; +} + bool NeedsOriginInWindow(ECaveGeneratorType Archetype) { // This is the same topology policy as VoxelForgePlayerFitWindow.h. That header is test-only, @@ -205,6 +268,7 @@ struct FExploreArguments bool bProfileDensityFull = false; bool bProfileLod = false; bool bOpBounds = false; + bool bProfileTileVerdictCache = false; bool bFailureFocusRender = false; FString OutDirectory; @@ -223,6 +287,10 @@ struct FExploreArguments float BudgetMinutes = DefaultBudgetMinutes; bool bSurfaceRoughnessOverride = false; float SurfaceRoughness = 0.0f; + bool bRoughnessNoiseTypeOverride = false; + EVoxelNoiseType RoughnessNoiseType = EVoxelNoiseType::FBM; + bool bRoughnessFrequencyOverride = false; + float RoughnessFrequency = 0.0f; bool bTunnelFloorEnabledOverride = false; int32 TunnelFloorEnabled = 1; bool bTunnelFloorTerracingOverride = false; @@ -237,6 +305,14 @@ struct FExploreArguments int32 TunnelFloorLedges = 0; bool bTunnelFloorMaxLedgesOverride = false; int32 TunnelFloorMaxLedges = 0; + bool bRoomMouthRiseOverride = false; + float RoomMouthRise = 0.0f; + bool bRoomMouthRiseBlendOverride = false; + float RoomMouthRiseBlend = 0.0f; + bool bPassageFloorWritesOverride = false; + int32 PassageFloorWrites = 1; + bool bRenderCameraOverride = false; + FVector RenderCamera = FVector::ZeroVector; FString CanonicalModes() const { @@ -285,6 +361,8 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull; OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod")); OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds")); + OutArguments.bProfileTileVerdictCache = FParse::Param( + *Params, TEXT("profiletileverdictcache")); FParse::Value(*Params, TEXT("out="), OutText); FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth); FParse::Value(*Params, TEXT("renderheight="), OutArguments.RenderHeight); @@ -317,6 +395,18 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr const bool bSurfaceRoughnessParsed = FParse::Value( *Params, TEXT("surfaceroughness="), OutArguments.SurfaceRoughness); OutArguments.bSurfaceRoughnessOverride = bSurfaceRoughnessSpecified; + FString RoughnessNoiseTypeText; + const bool bRoughnessNoiseTypeSpecified = Params.Contains( + TEXT("roughnesstype="), ESearchCase::IgnoreCase); + const bool bRoughnessNoiseTypeParsed = FParse::Value( + *Params, TEXT("roughnesstype="), RoughnessNoiseTypeText) + && ParseNoiseType(RoughnessNoiseTypeText, OutArguments.RoughnessNoiseType); + OutArguments.bRoughnessNoiseTypeOverride = bRoughnessNoiseTypeSpecified; + const bool bRoughnessFrequencySpecified = Params.Contains( + TEXT("roughnessfrequency="), ESearchCase::IgnoreCase); + const bool bRoughnessFrequencyParsed = FParse::Value( + *Params, TEXT("roughnessfrequency="), OutArguments.RoughnessFrequency); + OutArguments.bRoughnessFrequencyOverride = bRoughnessFrequencySpecified; const bool bTunnelFloorEnabledSpecified = Params.Contains( TEXT("tunnelfloor="), ESearchCase::IgnoreCase); const bool bTunnelFloorEnabledParsed = FParse::Value( @@ -352,6 +442,42 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr const bool bTunnelFloorMaxLedgesParsed = FParse::Value( *Params, TEXT("tunnelfloormaxledges="), OutArguments.TunnelFloorMaxLedges); OutArguments.bTunnelFloorMaxLedgesOverride = bTunnelFloorMaxLedgesSpecified; + const bool bRoomMouthRiseSpecified = Params.Contains( + TEXT("roommouthrise="), ESearchCase::IgnoreCase); + const bool bRoomMouthRiseParsed = FParse::Value( + *Params, TEXT("roommouthrise="), OutArguments.RoomMouthRise); + OutArguments.bRoomMouthRiseOverride = bRoomMouthRiseSpecified; + const bool bRoomMouthRiseBlendSpecified = Params.Contains( + TEXT("roommouthriseblend="), ESearchCase::IgnoreCase); + const bool bRoomMouthRiseBlendParsed = FParse::Value( + *Params, TEXT("roommouthriseblend="), OutArguments.RoomMouthRiseBlend); + OutArguments.bRoomMouthRiseBlendOverride = bRoomMouthRiseBlendSpecified; + const bool bPassageFloorWritesSpecified = Params.Contains( + TEXT("passagefloorwrites="), ESearchCase::IgnoreCase); + const bool bPassageFloorWritesParsed = FParse::Value( + *Params, TEXT("passagefloorwrites="), OutArguments.PassageFloorWrites); + OutArguments.bPassageFloorWritesOverride = bPassageFloorWritesSpecified; + + FString RenderCameraText; + const bool bRenderCameraSpecified = Params.Contains( + TEXT("rendercamera="), ESearchCase::IgnoreCase); + bool bRenderCameraParsed = false; + if (bRenderCameraSpecified) + { + const int32 CameraOffset = Params.Find(TEXT("rendercamera="), ESearchCase::IgnoreCase); + if (CameraOffset != INDEX_NONE) + { + RenderCameraText = Params.Mid(CameraOffset + 13); + const int32 CameraEnd = RenderCameraText.Find(TEXT(" ")); + if (CameraEnd != INDEX_NONE) + { + RenderCameraText.LeftInline(CameraEnd); + } + RenderCameraText.TrimQuotesInline(); + bRenderCameraParsed = ParseRenderCamera(RenderCameraText, OutArguments.RenderCamera); + } + } + OutArguments.bRenderCameraOverride = bRenderCameraSpecified; int32 FailureFocusRender = 0; FParse::Value(*Params, TEXT("failurefocus="), FailureFocusRender); OutArguments.bFailureFocusRender = FailureFocusRender != 0; @@ -488,6 +614,19 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr OutError = TEXT("surfaceroughness must be finite and greater than or equal to zero."); return false; } + if (OutArguments.bRoughnessNoiseTypeOverride && !bRoughnessNoiseTypeParsed) + { + OutError = TEXT("roughnesstype must be FBM, Ridged, Mixed, or Cellular."); + return false; + } + if (OutArguments.bRoughnessFrequencyOverride + && (!bRoughnessFrequencyParsed + || !FMath::IsFinite(OutArguments.RoughnessFrequency) + || OutArguments.RoughnessFrequency <= 0.0f)) + { + OutError = TEXT("roughnessfrequency must be finite and greater than zero."); + return false; + } if (OutArguments.bTunnelFloorEnabledOverride && (!bTunnelFloorEnabledParsed || (OutArguments.TunnelFloorEnabled != 0 @@ -544,6 +683,37 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr OutError = TEXT("tunnelfloormaxledges must be an integer in [1,4096]."); return false; } + if (OutArguments.bRoomMouthRiseOverride + && (!bRoomMouthRiseParsed + || !FMath::IsFinite(OutArguments.RoomMouthRise) + || OutArguments.RoomMouthRise < 0.0f + || OutArguments.RoomMouthRise > 1.0f)) + { + OutError = TEXT("roommouthrise must be finite and in [0,1]."); + return false; + } + if (OutArguments.bRoomMouthRiseBlendOverride + && (!bRoomMouthRiseBlendParsed + || !FMath::IsFinite(OutArguments.RoomMouthRiseBlend) + || OutArguments.RoomMouthRiseBlend < 8.0f + || OutArguments.RoomMouthRiseBlend > 128.0f)) + { + OutError = TEXT("roommouthriseblend must be finite and in [8,128]."); + return false; + } + if (OutArguments.bPassageFloorWritesOverride + && (!bPassageFloorWritesParsed + || (OutArguments.PassageFloorWrites != 0 + && OutArguments.PassageFloorWrites != 1))) + { + OutError = TEXT("passagefloorwrites must be 0 or 1."); + return false; + } + if (OutArguments.bRenderCameraOverride && !bRenderCameraParsed) + { + OutError = TEXT("rendercamera must be three finite comma-separated voxel coordinates."); + return false; + } if (OutText.IsEmpty()) { @@ -773,6 +943,11 @@ struct FExploreWorld OutError = TEXT("UVoxelStrateManager::Initialize refused the transient layout."); return false; } + if (Arguments.bPassageFloorWritesOverride) + { + Manager->SetPassageSupportFloorWritesEnabledForDiagnostics( + Arguments.PassageFloorWrites != 0); + } if (!Manager->GetLayout().IsValidIndex(Arguments.Slot)) { @@ -816,6 +991,27 @@ struct FExploreWorld break; } } + if (Arguments.bRoughnessNoiseTypeOverride + || Arguments.bRoughnessFrequencyOverride) + { + if (Arguments.Archetype != ECaveGeneratorType::TunnelNetwork + && Arguments.Archetype != ECaveGeneratorType::Underwater) + { + OutError = TEXT( + "roughnesstype/roughnessfrequency are only supported by TunnelNetwork or Underwater."); + return false; + } + if (Arguments.bRoughnessNoiseTypeOverride) + { + Target.Definition->GenerationParams.RoughnessNoiseType = + Arguments.RoughnessNoiseType; + } + if (Arguments.bRoughnessFrequencyOverride) + { + Target.Definition->GenerationParams.RoughnessFrequency = + Arguments.RoughnessFrequency; + } + } // Floor controls are applied only to the requested target definition. They are // commandlet overrides for measured archetype comparisons; authored assets still carry // the defaults/rolled values in FStrateGenerationParams. @@ -850,6 +1046,14 @@ struct FExploreWorld { TargetTunnelParams.TunnelFloorMaxLedges = Arguments.TunnelFloorMaxLedges; } + if (Arguments.bRoomMouthRiseOverride) + { + TargetTunnelParams.RoomMouthRiseStrength = Arguments.RoomMouthRise; + } + if (Arguments.bRoomMouthRiseBlendOverride) + { + TargetTunnelParams.RoomMouthRiseBlendVoxels = Arguments.RoomMouthRiseBlend; + } TargetBottomWorldZ = Target.BottomChunkZ * CHUNK_SIZE; TargetTopWorldZ = (Target.TopChunkZ + 1) * CHUNK_SIZE; TargetBoundarySealThickness = BoundarySealForArchetype( @@ -2388,6 +2592,11 @@ bool RunRender( CameraSeeds.Emplace(Position, FString(Source)); }; + if (Arguments.bRenderCameraOverride) + { + AddCameraSeed(true, Arguments.RenderCamera, TEXT("render_camera_override")); + } + for (int32 SeedIndex = 0; SeedIndex < CameraSeedWalk.CameraSeedPoses.Num(); ++SeedIndex) { const FString Source = CameraSeedWalk.CameraSeedSources.IsValidIndex(SeedIndex) @@ -3797,6 +4006,13 @@ void WriteTunnelFloorOverrides( { Writer.WriteValue(TEXT("max_ledges"), Arguments.TunnelFloorMaxLedges); } + Writer.WriteValue(TEXT("passage_support_floor_writes_override"), + Arguments.bPassageFloorWritesOverride); + if (Arguments.bPassageFloorWritesOverride) + { + Writer.WriteValue(TEXT("passage_support_floor_writes"), + Arguments.PassageFloorWrites != 0); + } Writer.WriteObjectEnd(); } @@ -3836,12 +4052,44 @@ FString BuildManifestJson( Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); WriteTunnelFloorOverrides(*Writer, Arguments); + Writer->WriteValue(TEXT("room_mouth_rise_override"), Arguments.bRoomMouthRiseOverride); + if (Arguments.bRoomMouthRiseOverride) + { + Writer->WriteValue(TEXT("room_mouth_rise_strength"), + static_cast(Arguments.RoomMouthRise)); + } + Writer->WriteValue(TEXT("room_mouth_rise_blend_override"), + Arguments.bRoomMouthRiseBlendOverride); + if (Arguments.bRoomMouthRiseBlendOverride) + { + Writer->WriteValue(TEXT("room_mouth_rise_blend_voxels"), + static_cast(Arguments.RoomMouthRiseBlend)); + } Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride); if (Arguments.bSurfaceRoughnessOverride) { Writer->WriteValue(TEXT("surface_roughness"), static_cast(Arguments.SurfaceRoughness)); } + Writer->WriteValue(TEXT("roughness_noise_type_override"), + Arguments.bRoughnessNoiseTypeOverride); + if (Arguments.bRoughnessNoiseTypeOverride) + { + Writer->WriteValue(TEXT("roughness_noise_type"), + NoiseTypeName(Arguments.RoughnessNoiseType)); + } + Writer->WriteValue(TEXT("roughness_frequency_override"), + Arguments.bRoughnessFrequencyOverride); + if (Arguments.bRoughnessFrequencyOverride) + { + Writer->WriteValue(TEXT("roughness_frequency"), + static_cast(Arguments.RoughnessFrequency)); + } + Writer->WriteValue(TEXT("render_camera_override"), Arguments.bRenderCameraOverride); + if (Arguments.bRenderCameraOverride) + { + WriteJsonVector(*Writer, TEXT("render_camera_voxels"), Arguments.RenderCamera, 1.0f); + } WriteJsonIntVector(*Writer, TEXT("region_origin_voxels"), Export.RegionOrigin); WriteJsonBounds(*Writer, TEXT("region_bounds_voxels"), Export.RegionOrigin, Export.RegionSize, 0.25f); Writer->WriteObjectStart(TEXT("region_bounds_metres")); @@ -4055,12 +4303,39 @@ FString BuildExploreJson( Writer->WriteValue(TEXT("slot"), Arguments.Slot); Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); WriteTunnelFloorOverrides(*Writer, Arguments); + Writer->WriteValue(TEXT("room_mouth_rise_override"), Arguments.bRoomMouthRiseOverride); + if (Arguments.bRoomMouthRiseOverride) + { + Writer->WriteValue(TEXT("room_mouth_rise_strength"), + static_cast(Arguments.RoomMouthRise)); + } + Writer->WriteValue(TEXT("room_mouth_rise_blend_override"), + Arguments.bRoomMouthRiseBlendOverride); + if (Arguments.bRoomMouthRiseBlendOverride) + { + Writer->WriteValue(TEXT("room_mouth_rise_blend_voxels"), + static_cast(Arguments.RoomMouthRiseBlend)); + } Writer->WriteValue(TEXT("surface_roughness_override"), Arguments.bSurfaceRoughnessOverride); if (Arguments.bSurfaceRoughnessOverride) { Writer->WriteValue(TEXT("surface_roughness"), static_cast(Arguments.SurfaceRoughness)); } + Writer->WriteValue(TEXT("roughness_noise_type_override"), + Arguments.bRoughnessNoiseTypeOverride); + if (Arguments.bRoughnessNoiseTypeOverride) + { + Writer->WriteValue(TEXT("roughness_noise_type"), + NoiseTypeName(Arguments.RoughnessNoiseType)); + } + Writer->WriteValue(TEXT("roughness_frequency_override"), + Arguments.bRoughnessFrequencyOverride); + if (Arguments.bRoughnessFrequencyOverride) + { + Writer->WriteValue(TEXT("roughness_frequency"), + static_cast(Arguments.RoughnessFrequency)); + } Writer->WriteArrayStart(TEXT("modes")); if (Arguments.bRender) Writer->WriteValue(TEXT("render")); if (Arguments.bWalk) Writer->WriteValue(TEXT("walk")); @@ -4077,6 +4352,8 @@ FString BuildExploreJson( Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity); Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull); Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod); + Writer->WriteValue(TEXT("profile_tile_verdict_cache"), + Arguments.bProfileTileVerdictCache); Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds); Writer->WriteValue(TEXT("budget_minutes"), static_cast(Arguments.BudgetMinutes)); Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory); @@ -5133,6 +5410,66 @@ bool RunOpBounds( return true; } +bool RunTileVerdictCacheProbe( + const FExploreArguments& Arguments, + FExploreWorld& World, + FString& OutError) +{ + if (!World.Generator) + { + OutError = TEXT("tile-verdict cache probe has no generator."); + return false; + } + + // Deliberately use a region well above the synthetic layout. The first 32-cell proof is a + // genuine enclosing-region certificate; the 8-cell requests below are aligned subregions of + // that same lattice. No generated cave data is involved in this probe. + const FIntVector RegionOrigin(-32, -32, World.TargetTopWorldZ + 1024); + const EVoxelTileClass RegionVerdict = World.Generator->ClassifyTile( + RegionOrigin, 1, 32); + if (RegionVerdict != EVoxelTileClass::AllAir) + { + OutError = FString::Printf( + TEXT("tile-verdict cache probe expected an all-air enclosing region, got %d."), + static_cast(RegionVerdict)); + return false; + } + + auto ClassifyChildren = [&World, &RegionOrigin]() + { + for (int32 Z = 0; Z < 32; Z += 8) + for (int32 Y = 0; Y < 32; Y += 8) + for (int32 X = 0; X < 32; X += 8) + { + World.Generator->ClassifyTile( + RegionOrigin + FIntVector(X, Y, Z), 1, 8); + } + }; + + ClassifyChildren(); + World.Generator->ClassifyTile(RegionOrigin, 1, 32); + ClassifyChildren(); + + const VoxelDensityProfile::FSnapshot Snapshot = VoxelDensityProfile::Snapshot(); + const int32 CallsIndex = static_cast( + VoxelDensityProfile::ECounter::TileClassifyCalls); + const int32 HitsIndex = static_cast( + VoxelDensityProfile::ECounter::TileVerdictCacheHits); + const int32 RegionHitsIndex = static_cast( + VoxelDensityProfile::ECounter::TileVerdictRegionHits); + const int32 StoresIndex = static_cast( + VoxelDensityProfile::ECounter::TileVerdictCacheStores); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeTileVerdictCache] probe region=(%d,%d,%d) cells=32 verdict=AllAir " + "calls=%llu hits=%llu region_hits=%llu stores=%llu"), + RegionOrigin.X, RegionOrigin.Y, RegionOrigin.Z, + static_cast(Snapshot.Counters[CallsIndex]), + static_cast(Snapshot.Counters[HitsIndex]), + static_cast(Snapshot.Counters[RegionHitsIndex]), + static_cast(Snapshot.Counters[StoresIndex])); + return true; +} + } // namespace UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet() @@ -5189,6 +5526,16 @@ int32 RunExploreCase(const FString& Params, FString* OutJson) } const double SetupSeconds = FPlatformTime::Seconds() - SetupStartSeconds; + if (Arguments.bProfileTileVerdictCache) + { + if (!RunTileVerdictCacheProbe(Arguments, World, Error)) + { + UE_LOG(LogTemp, Error, + TEXT("[VoxelForgeExplore] tile-verdict cache probe failed: %s"), *Error); + return 1; + } + } + bool bRequestedModeFailed = false; if (Arguments.bOpBounds) { @@ -5303,18 +5650,36 @@ int32 RunExploreCase(const FString& Params, FString* OutJson) } else if (Arguments.bRender) { - // A render-only invocation still has to obey the inside-walkable-space contract. Run the - // same focused walk as a private seed pass; it is intentionally omitted from the render-only - // JSON so the selected mode remains render. - const double Start = FPlatformTime::Seconds(); - const bool bWalkOk = RunWalk(Arguments, World, RenderSeedWalk, Budget); - Output.WalkSeconds = FPlatformTime::Seconds() - Start; - if (!bWalkOk && !Budget.bTruncated) + if (Arguments.bRenderCameraOverride) { - bRequestedModeFailed = true; + // Controlled diagnostic renders can supply a player-fit seed captured from a baseline + // run. This keeps the mesh region, camera pose, and raster target identical while a + // single generator writer is toggled. The density guard in RunRender still refuses a + // stale/inside-rock pose. + RenderSeedWalk.CameraSeedPoses.Add(Arguments.RenderCamera); + RenderSeedWalk.CameraSeedSources.Add(TEXT("render_camera_override")); + RenderSeedWalk.Status = TEXT("override"); + Output.WalkSeconds = 0.0; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeExplore] render camera seed override (%.3f, %.3f, %.3f)"), + Arguments.RenderCamera.X, Arguments.RenderCamera.Y, Arguments.RenderCamera.Z); + } + else + { + // A render-only invocation still has to obey the inside-walkable-space contract. Run + // the same focused walk as a private seed pass; it is intentionally omitted from the + // render-only JSON so the selected mode remains render. + const double Start = FPlatformTime::Seconds(); + const bool bWalkOk = RunWalk(Arguments, World, RenderSeedWalk, Budget); + Output.WalkSeconds = FPlatformTime::Seconds() - Start; + if (!bWalkOk && !Budget.bTruncated) + { + bRequestedModeFailed = true; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeExplore] render camera seed walk %.3fs (%s)"), + Output.WalkSeconds, *RenderSeedWalk.Status); } - UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore] render camera seed walk %.3fs (%s)"), - Output.WalkSeconds, *RenderSeedWalk.Status); CameraSeedWalk = &RenderSeedWalk; // Keep the private render seed capture available to the run-level shared-grid report; // the walk object is still omitted from the public JSON when walk was not requested. @@ -5944,6 +6309,9 @@ bool BuildBatchCaseParams( { TEXT("render_step"), TEXT("renderstep") }, { TEXT("render_max_distance"), TEXT("rendermaxdistance") }, { TEXT("surface_roughness"), TEXT("surfaceroughness") }, + { TEXT("roughness_frequency"), TEXT("roughnessfrequency") }, + { TEXT("room_mouth_rise_strength"), TEXT("roommouthrise") }, + { TEXT("room_mouth_rise_blend_voxels"), TEXT("roommouthriseblend") }, { TEXT("tunnel_floor_step_height"), TEXT("tunnelfloorstep") }, { TEXT("tunnel_floor_max_ledge_height"), TEXT("tunnelfloormaxledge") }, { TEXT("tunnel_floor_gentle_slope"), TEXT("tunnelfloorgentle") }, @@ -5956,6 +6324,12 @@ bool BuildBatchCaseParams( } } + FString RoughnessType; + if (JsonString(Case, TEXT("roughness_noise_type"), RoughnessType)) + { + OutParams += FString::Printf(TEXT(" -roughnesstype=%s"), *RoughnessType); + } + const TPair IntegerFloorFields[] = { { TEXT("tunnel_floor_ledge_count"), TEXT("tunnelfloorledges") }, { TEXT("tunnel_floor_max_ledges"), TEXT("tunnelfloormaxledges") },