diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index b447551..1a6b28d 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -41,6 +41,40 @@ namespace { + /** + * Concrete block thunks use the scalar body with the concrete operator type visible to the + * compiler. This is deliberately not a second numerical implementation: the scalar Eval is + * still the single source of truth, but the op-major caller pays one virtual dispatch for the + * block and the inner calls can inline/devirtualize. Operators with a block-native kernel can + * replace this helper later without changing the stack contract. + */ + template + FORCEINLINE void VF_EvalBlockByScalar(const TOp& Op, const FVoxelOpBlock& Block) + { + if (Block.Samples == nullptr || Block.Step <= 0 + || Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0) + { + return; + } + + for (int32 Z = 0; Z < Block.SizeZ; ++Z) + { + for (int32 Y = 0; Y < Block.SizeY; ++Y) + { + for (int32 X = 0; X < Block.SizeX; ++X) + { + FVoxelOpSample& Sample = Block.At(X, Y, Z); + Op.PrepareBlockSample(Sample); + Op.Eval( + static_cast(Block.OriginVoxels.X + X * Block.Step), + static_cast(Block.OriginVoxels.Y + Y * Block.Step), + static_cast(Block.OriginVoxels.Z + Z * Block.Step), + Sample); + } + } + } + } + /** * BORNE **PROUVABLE** DE `|Perlin3D|`, ET ELLE N'EST PAS 1.0. * @@ -516,6 +550,11 @@ namespace const TCHAR* DebugName() const override { return TEXT("ConstantFieldSource"); } + void EvalBlock(const FVoxelOpBlock& Block) const override + { + VF_EvalBlockByScalar(*this, Block); + } + private: float Value; }; @@ -1839,6 +1878,11 @@ namespace const TCHAR* DebugName() const override { return TEXT("SdfConvertOp"); } + void EvalBlock(const FVoxelOpBlock& Block) const override + { + VF_EvalBlockByScalar(*this, Block); + } + private: bool IsInactive(const FVoxelBoxHypotheses& H) const { @@ -1926,6 +1970,11 @@ namespace const TCHAR* DebugName() const override { return TEXT("OriginSpineOp"); } + void EvalBlock(const FVoxelOpBlock& Block) const override + { + VF_EvalBlockByScalar(*this, Block); + } + private: float TopZ, BotZ, Seal, Base, Radius; }; @@ -2007,6 +2056,11 @@ namespace const TCHAR* DebugName() const override { return TEXT("BoundarySealOp"); } + void EvalBlock(const FVoxelOpBlock& Block) const override + { + VF_EvalBlockByScalar(*this, Block); + } + private: float TopZ, BotZ, Thickness, Base; }; @@ -2074,6 +2128,11 @@ namespace bool IsXYPure() const override { return true; } const TCHAR* DebugName() const override { return TEXT("XYEdgeSealOp"); } + void EvalBlock(const FVoxelOpBlock& Block) const override + { + VF_EvalBlockByScalar(*this, Block); + } + private: float Base = 8.0f; mutable float WorldRadius = 8192.0f; @@ -2186,6 +2245,11 @@ namespace const TCHAR* DebugName() const override { return TEXT("PassageCarveOp"); } + void EvalBlock(const FVoxelOpBlock& Block) const override + { + VF_EvalBlockByScalar(*this, Block); + } + private: TWeakObjectPtr Manager; float Base, Seal, TopZ, BotZ, SpineRadius; @@ -3786,6 +3850,15 @@ namespace * Lu par les arches, les dômes, le pincement et le biais de sol. */ int32 GetNearestRoomIdx() const { return State().NearestRoom; } + /** Restore the source-owned state belonging to one sample before a later block op reads it. */ + void RestoreBlockSample(const FVoxelOpSample& Sample) const + { + FState& S = State(); + S.ActiveCache = Sample.RoomCache; + S.NearestRoom = Sample.NearestRoomIndex; + S.bLocalParamsValid = false; + } + void BuildSupportColumn( float WorldX, float WorldY, const FChunkSDFCache& Cache, @@ -4255,6 +4328,16 @@ namespace } } } + + // These are the per-sample hand-offs needed by an op-major block. The scalar path + // continues to use FState directly; publishing them here only makes the dependency + // explicit for a later modifier and does not alter Density/Sdf. + InOut.RoomCache = &GetCache(); + InOut.NearestRoomIndex = S.NearestRoom; + InOut.bHasTunnelCoreWorldEvaluation = true; + InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF; + InOut.bTunnelCoreSupportFloor = S.LastTunnelCoreWorldEvaluation.bSupportFloor; + InOut.bTunnelCoreRoomFloor = S.LastTunnelCoreWorldEvaluation.bRoomFloor; } //--------------------------------------------------------------------- @@ -6629,6 +6712,11 @@ namespace const TCHAR* DebugName() const override { return TEXT("RoomGraphSource"); } + void EvalBlock(const FVoxelOpBlock& Block) const override + { + VF_EvalBlockByScalar(*this, Block); + } + private: FStrateGenerationParams P; int32 Seed; @@ -6725,6 +6813,7 @@ namespace bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} const TCHAR* DebugName() const override { return TEXT("CaveRoughnessMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -6957,7 +7046,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("CaveTerraceMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -7106,7 +7200,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("LayerLineMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override { @@ -7203,7 +7302,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("RibbingMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override { @@ -7295,7 +7399,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("CaveOverhangMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -7408,7 +7517,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("CaveCliffMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -7518,7 +7632,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("ScallopMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -7620,7 +7739,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("CaveArchMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -7749,7 +7873,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("RoomColumnMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float, FVoxelOpSample& InOut) const override { @@ -7841,7 +7970,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("DomeMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -7981,7 +8115,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("PinchMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override { @@ -8107,7 +8246,12 @@ namespace EVoxelOpResourceMask RequiredResources() const override { return VoxelOpResources::RoomGeometry; } bool IsAdditive() const override { return true; } void PrepareChunk(const FVoxelOpContext&) override {} + void PrepareBlockSample(const FVoxelOpSample& Sample) const override + { + if (Rooms != nullptr) { Rooms->RestoreBlockSample(Sample); } + } const TCHAR* DebugName() const override { return TEXT("FloorBiasMod"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override { @@ -8424,6 +8568,7 @@ namespace } const TCHAR* DebugName() const override { return TEXT("WormFieldSource"); } + void EvalBlock(const FVoxelOpBlock& Block) const override { VF_EvalBlockByScalar(*this, Block); } private: FStrateGenerationParams P; diff --git a/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Source/VoxelForge/Private/VoxelDensityProfile.cpp index 7e19104..5062e84 100644 --- a/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -301,6 +301,48 @@ namespace VoxelDensityProfile return Result; } + FSnapshot SnapshotCurrentThread() + { + FSnapshot Result; + FThreadState* State = GThreadState; + if (State == nullptr) + { + Result.TimerPairCycles = GetTimerPairCycles(); + Result.SampleInterval = GetSampleInterval(); + Result.Mode = GetMode(); + return Result; + } + + for (int32 Index = 0; Index < BucketCount; ++Index) + { + Result.Calls[Index] = State->Calls[Index]; + Result.Samples[Index] = State->Samples[Index]; + if (State->Samples[Index] == 0) + { + Result.Cycles[Index] = State->Cycles[Index]; + } + else + { + const double Scale = static_cast(State->Calls[Index]) + / static_cast(State->Samples[Index]); + const double EstimatedCycles = static_cast(State->Cycles[Index]) * Scale; + Result.Cycles[Index] = EstimatedCycles > static_cast(MAX_uint64) + ? MAX_uint64 + : static_cast(EstimatedCycles + 0.5); + } + Result.WallCycles[Index] = Result.Cycles[Index]; + Result.ActiveWorkers[Index] = State->Calls[Index] > 0 ? 1 : 0; + } + for (int32 Index = 0; Index < CounterCount; ++Index) + { + Result.Counters[Index] = State->Counters[Index]; + } + Result.TimerPairCycles = GetTimerPairCycles(); + Result.SampleInterval = GetSampleInterval(); + Result.Mode = GetMode(); + return Result; + } + void AddCounter(ECounter Counter, uint64 Amount) { const int32 Index = static_cast(Counter); @@ -441,6 +483,11 @@ namespace VoxelDensityProfile case ECounter::TileVerdictCacheHits: return TEXT("TileVerdictCacheHits"); case ECounter::TileVerdictRegionHits: return TEXT("TileVerdictRegionHits"); case ECounter::TileVerdictCacheStores: return TEXT("TileVerdictCacheStores"); + case ECounter::OpBlockBuilds: return TEXT("OpBlockBuilds"); + case ECounter::OpBlockSamples: return TEXT("OpBlockSamples"); + case ECounter::OpBlockOperators: return TEXT("OpBlockOperators"); + case ECounter::OpBlockActiveOperators: return TEXT("OpBlockActiveOperators"); + case ECounter::OpBlockPrunedOperators: return TEXT("OpBlockPrunedOperators"); case ECounter::Count: break; } return TEXT("Unknown"); diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 0406dc7..395ad71 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -178,6 +178,193 @@ static thread_local FSurfaceColumnCache GSurfColCache; // T2.b — per-thread octave bias for the tile being meshed (see VoxelGenerator.h). // 0 = full quality; set by the mesher per tile from Step + Settings->LODOctaveDrop. thread_local int32 VoxelGenLOD::OctaveBias = 0; + +//============================================================================= +// OP-MAJOR DENSITY BLOCK HAND-OFF +//============================================================================= +// The public Begin/End pair is only a lifetime marker. The actual storage is worker-local and +// bounded by the current mesher lattice. A tile may cross several chunk keys; each key is filled +// by the already-prepared stack that GetDensityAt selected for that key, so no mutable stack state +// is shared across chunk boundaries. +struct FVoxelDensityBlockSession +{ + bool bActive = false; + FIntVector Origin = FIntVector::ZeroValue; + int32 Step = 1; + int32 SizeX = 0; + int32 SizeY = 0; + int32 SizeZ = 0; + TArray Samples; + TArray Valid; + TArray BlockScratch; + TArray ActiveOps; + + void Begin(FIntVector InOrigin, int32 InStep, int32 InSizeX, int32 InSizeY, int32 InSizeZ) + { + bActive = InStep > 0 && InSizeX > 0 && InSizeY > 0 && InSizeZ > 0; + Origin = InOrigin; + Step = FMath::Max(InStep, 1); + SizeX = FMath::Max(InSizeX, 0); + SizeY = FMath::Max(InSizeY, 0); + SizeZ = FMath::Max(InSizeZ, 0); + if (!bActive) + { + Samples.Reset(); + Valid.Reset(); + BlockScratch.Reset(); + ActiveOps.Reset(); + return; + } + + // Allocate lazily on the first operator-stack sample. Legacy archetypes still use the + // ordinary scalar generator and should not pay for a block scratch lattice merely because + // the mesher opened the scoped hand-off. + BlockScratch.Reset(); + } + + void End() + { + bActive = false; + // Keep capacity for the next tile on this worker, but never retain a verdict between + // sessions. A stale sample can only cost the next task a block fill, never change it. + Samples.Reset(); + Valid.Reset(); + BlockScratch.Reset(); + ActiveOps.Reset(); + } + + int32 Index(int32 X, int32 Y, int32 Z) const + { + return (Z * SizeY + Y) * SizeX + X; + } + + bool LocalIndex(FIntVector World, int32& OutX, int32& OutY, int32& OutZ) const + { + const int32 DX = World.X - Origin.X; + const int32 DY = World.Y - Origin.Y; + const int32 DZ = World.Z - Origin.Z; + if (DX % Step != 0 || DY % Step != 0 || DZ % Step != 0) { return false; } + OutX = DX / Step; + OutY = DY / Step; + OutZ = DZ / Step; + return OutX >= 0 && OutX < SizeX + && OutY >= 0 && OutY < SizeY + && OutZ >= 0 && OutZ < SizeZ; + } + + bool FindAxisRange(int32 Axis, int32 TargetChunk, int32& OutMin, int32& OutMax) const + { + OutMin = INT32_MAX; + OutMax = INT32_MIN; + const int32 Limit = Axis == 0 ? SizeX : (Axis == 1 ? SizeY : SizeZ); + const int32 Base = Axis == 0 ? Origin.X : (Axis == 1 ? Origin.Y : Origin.Z); + for (int32 I = 0; I < Limit; ++I) + { + const int32 Coordinate = Base + I * Step; + if (FMath::FloorToInt(static_cast(Coordinate) / CHUNK_SIZE) == TargetChunk) + { + OutMin = FMath::Min(OutMin, I); + OutMax = FMath::Max(OutMax, I); + } + } + return OutMin != INT32_MAX; + } + + bool FillChunk(const FVoxelOpStack& Stack, FIntVector ChunkCoord, FIntVector World, + FVoxelOpSample& OutSample) + { + int32 X = 0, Y = 0, Z = 0; + if (!LocalIndex(World, X, Y, Z)) { return false; } + const int32 Count = SizeX * SizeY * SizeZ; + if (Samples.Num() != Count || Valid.Num() != Count) + { + Samples.SetNum(Count); + Valid.Init(0, Count); + } + const int32 GlobalIndex = Index(X, Y, Z); + if (Valid[GlobalIndex] != 0) + { + OutSample = Samples[GlobalIndex]; + return true; + } + + int32 MinX = 0, MaxX = 0, MinY = 0, MaxY = 0, MinZ = 0, MaxZ = 0; + if (!FindAxisRange(0, ChunkCoord.X, MinX, MaxX) + || !FindAxisRange(1, ChunkCoord.Y, MinY, MaxY) + || !FindAxisRange(2, ChunkCoord.Z, MinZ, MaxZ)) + { + return false; + } + + const int32 BlockSizeX = MaxX - MinX + 1; + const int32 BlockSizeY = MaxY - MinY + 1; + const int32 BlockSizeZ = MaxZ - MinZ + 1; + const int32 BlockCount = BlockSizeX * BlockSizeY * BlockSizeZ; + BlockScratch.SetNum(BlockCount); + for (FVoxelOpSample& Sample : BlockScratch) + { + Sample = FVoxelOpSample(); + } + + FVoxelOpBlock Block; + Block.OriginVoxels = Origin + FIntVector(MinX * Step, MinY * Step, MinZ * Step); + Block.Step = Step; + Block.SizeX = BlockSizeX; + Block.SizeY = BlockSizeY; + Block.SizeZ = BlockSizeZ; + Block.Samples = BlockScratch.GetData(); + const FIntVector BlockLast = Block.OriginVoxels + + FIntVector((BlockSizeX - 1) * Step, + (BlockSizeY - 1) * Step, + (BlockSizeZ - 1) * Step); + const FBox BlockBox( + FVector(static_cast(Block.OriginVoxels.X), + static_cast(Block.OriginVoxels.Y), + static_cast(Block.OriginVoxels.Z)), + FVector(static_cast(BlockLast.X), + static_cast(BlockLast.Y), + static_cast(BlockLast.Z))); + const int32 ActiveCount = Stack.BuildActiveOpList( + BlockBox, Step, Block.OriginVoxels, ActiveOps); + Stack.EvalBlock(Block, ActiveOps); + + for (int32 Bz = 0; Bz < BlockSizeZ; ++Bz) + { + for (int32 By = 0; By < BlockSizeY; ++By) + { + for (int32 Bx = 0; Bx < BlockSizeX; ++Bx) + { + const int32 Global = Index(MinX + Bx, MinY + By, MinZ + Bz); + const int32 Local = (Bz * BlockSizeY + By) * BlockSizeX + Bx; + Samples[Global] = BlockScratch[Local]; + Valid[Global] = 1; + } + } + } + + if (VoxelDensityProfile::AreCountersEnabled()) + { + VoxelDensityProfile::AddCounter(VoxelDensityProfile::ECounter::OpBlockBuilds); + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::OpBlockSamples, + static_cast(BlockCount)); + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::OpBlockOperators, + static_cast(Stack.Num())); + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::OpBlockActiveOperators, + static_cast(ActiveCount)); + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::OpBlockPrunedOperators, + static_cast(FMath::Max(Stack.Num() - ActiveCount, 0))); + } + + OutSample = Samples[GlobalIndex]; + return true; + } +}; + +static thread_local FVoxelDensityBlockSession GVoxelDensityBlockSession; thread_local int32 VoxelGenLOD::SampleStep = 1; int32 VoxelGenLOD::GetThreadOctaveBias() @@ -1489,6 +1676,17 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings) InvalidateTileVerdictCache(); } +void UVoxelGenerator::BeginDensityBlock(FIntVector OriginVoxels, int32 Step, + int32 SizeX, int32 SizeY, int32 SizeZ) const +{ + GVoxelDensityBlockSession.Begin(OriginVoxels, Step, SizeX, SizeY, SizeZ); +} + +void UVoxelGenerator::EndDensityBlock() const +{ + GVoxelDensityBlockSession.End(); +} + float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt); @@ -1958,12 +2156,36 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co VoxelDensityProfile::FScopedTimer DensityCoreTimer( VoxelDensityProfile::EBucket::DensityCore); + FVoxelOpSample BlockCoreSample; + bool bUsedOpBlockSample = false; + // Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention // MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche // de diff qui suivent ne voient aucune différence. if (CP_UseOpStack) { - Result = ActiveOpStack->EvalMC(WorldX, WorldY, WorldZ); + const bool bIntegerLatticePoint = + WorldX == FMath::FloorToFloat(WorldX) + && WorldY == FMath::FloorToFloat(WorldY) + && WorldZ == FMath::FloorToFloat(WorldZ); + const bool bBlockSafeGenerator = + CP_GenType == ECaveGeneratorType::TunnelNetwork + || CP_GenType == ECaveGeneratorType::Underwater; + if (bIntegerLatticePoint && bBlockSafeGenerator + && GVoxelDensityBlockSession.bActive) + { + bUsedOpBlockSample = GVoxelDensityBlockSession.FillChunk( + *ActiveOpStack, + ChunkCoord, + FIntVector( + FMath::RoundToInt(WorldX), + FMath::RoundToInt(WorldY), + FMath::RoundToInt(WorldZ)), + BlockCoreSample); + } + Result = bUsedOpBlockSample + ? -BlockCoreSample.Density + : ActiveOpStack->EvalMC(WorldX, WorldY, WorldZ); } else switch (CP_GenType) { @@ -2028,8 +2250,20 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co bool bHavePreDisturbanceTunnelCore = false; if (CP_UseOpStack) { - bHavePreDisturbanceTunnelCore = ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation( - PreDisturbanceTunnelCore); + if (bUsedOpBlockSample && BlockCoreSample.bHasTunnelCoreWorldEvaluation) + { + PreDisturbanceTunnelCore.SDF = BlockCoreSample.TunnelCoreWorldSDF; + PreDisturbanceTunnelCore.bSupportFloor = + BlockCoreSample.bTunnelCoreSupportFloor; + PreDisturbanceTunnelCore.bRoomFloor = + BlockCoreSample.bTunnelCoreRoomFloor; + bHavePreDisturbanceTunnelCore = true; + } + else + { + bHavePreDisturbanceTunnelCore = ActiveOpStack->TryGetLastTunnelCoreWorldEvaluation( + PreDisturbanceTunnelCore); + } } if (!bHavePreDisturbanceTunnelCore && ActiveTunnelCoreCache->bValid) { diff --git a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index 24bfac1..300bc9f 100644 --- a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -5,6 +5,29 @@ #include "MarchingCubesTables.h" #include "VoxelDensityProfile.h" #include "ProfilingDebugging/CpuProfilerTrace.h" +#include "HAL/IConsoleManager.h" +#include "Misc/CommandLine.h" + +namespace +{ + int32 GVoxelForgeUseOperatorBlock = 1; + FAutoConsoleVariableRef CVarVoxelForgeUseOperatorBlock( + TEXT("voxel.UseOperatorBlock"), + GVoxelForgeUseOperatorBlock, + TEXT("Use the operator-major density block path for non-shared mesher grids.")); + bool GVoxelForgeBlockSwitchParsed = false; + + void VF_ParseOperatorBlockSwitch() + { + if (GVoxelForgeBlockSwitchParsed) { return; } + GVoxelForgeBlockSwitchParsed = true; + int32 CommandLineValue = GVoxelForgeUseOperatorBlock; + if (FParse::Value(FCommandLine::Get(), TEXT("voxel.UseOperatorBlock="), CommandLineValue)) + { + GVoxelForgeUseOperatorBlock = CommandLineValue; + } + } +} //============================================================================= // MAIN ALGORITHM @@ -17,6 +40,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 BandZMinVox, int32 BandZMaxVox) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh); + VF_ParseOperatorBlockSwitch(); FVoxelMeshData MeshData; if (OutCaptureGrid) { OutCaptureGrid->Reset(); } if (!Generator) return MeshData; @@ -420,12 +444,30 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, // gradient) sont échantillonnées — le reste du tampon reste non initialisé et non lu. VoxelDensityProfile::FScopedTimer MesherDensityGridTimer( VoxelDensityProfile::EBucket::MesherDensityGrid); + const bool bUseOperatorBlock = !bUseSharedDensityGrid + && GVoxelForgeUseOperatorBlock != 0; + if (bUseOperatorBlock) + { + // The grid includes one sample of halo on every side. The generator groups these exact + // lattice points by chunk key and lets the prepared op stack fill each group once. + Generator->BeginDensityBlock( + OriginVoxels + FIntVector(-Step, -Step, GzLo * Step), + Step, MDim, MDim, GzHi - GzLo + 1); + } for (int32 gz = GzLo; gz <= GzHi; gz++) { - if (ShouldAbortWork()) return FVoxelMeshData(); + if (ShouldAbortWork()) + { + if (bUseOperatorBlock) { Generator->EndDensityBlock(); } + return FVoxelMeshData(); + } for (int32 gy = -1; gy <= GridDim; gy++) { - if (ShouldAbortWork()) return FVoxelMeshData(); + if (ShouldAbortWork()) + { + if (bUseOperatorBlock) { Generator->EndDensityBlock(); } + return FVoxelMeshData(); + } for (int32 gx = -1; gx <= GridDim; gx++) { const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1); @@ -448,6 +490,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, } } } + if (bUseOperatorBlock) + { + Generator->EndDensityBlock(); + } MesherDensityGridTimer.End(); if (bUseProofBlocks) diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 6641634..f50eb86 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -12,6 +12,7 @@ #include "VoxelContentManager.h" #include "VoxelDensityVolume.h" #include "VoxelDensityOpStack.h" +#include "VoxelDensityProfile.h" #include "VoxelStats.h" // IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc // APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController était @@ -49,6 +50,17 @@ namespace GVoxelForgeProfileTileGeneration, TEXT("Log worker tile generation time with level/step/cell count.")); + int32 GVoxelForgeProfileDensity = 0; + FAutoConsoleVariableRef CVarVoxelForgeProfileDensity( + TEXT("voxel.ProfileDensity"), + GVoxelForgeProfileDensity, + TEXT("Collect sampled per-density profiling for the game path.")); + int32 GVoxelForgeProfileDensityFull = 0; + FAutoConsoleVariableRef CVarVoxelForgeProfileDensityFull( + TEXT("voxel.ProfileDensityFull"), + GVoxelForgeProfileDensityFull, + TEXT("Collect full per-operator profiling for the game path.")); + // Startup command-line cvars can arrive before the game world has spawned AVoxelWorld (or // before its first tiles have become visible). Keep the diagnostic edit pending until Tick has // a loaded tile; otherwise a startup measurement silently becomes a no-op or edits the wrong @@ -972,6 +984,39 @@ void AVoxelWorld::BeginPlay() Super::BeginPlay(); bShuttingDown.store(false, std::memory_order_relaxed); + // Some headless harnesses load module CVars after the engine's startup-CVar pass. Read the + // profiling switches explicitly as well, so a reported game profile cannot silently run with + // the default mode because of startup ordering. + int32 CommandLineProfile = GVoxelForgeProfileDensity; + int32 CommandLineProfileFull = GVoxelForgeProfileDensityFull; + if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensity="), CommandLineProfile)) + { + GVoxelForgeProfileDensity = CommandLineProfile; + } + if (FParse::Value(FCommandLine::Get(), TEXT("voxel.ProfileDensityFull="), CommandLineProfileFull)) + { + GVoxelForgeProfileDensityFull = CommandLineProfileFull; + } + + if (GVoxelForgeProfileDensityFull != 0) + { + VoxelDensityProfile::SetMode( + VoxelDensityProfile::EMode::Full, + VoxelDensityProfile::DefaultSampleInterval); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeDensityProfile] game_mode=full sample_interval=%u"), + VoxelDensityProfile::DefaultSampleInterval); + } + else if (GVoxelForgeProfileDensity != 0) + { + VoxelDensityProfile::SetMode( + VoxelDensityProfile::EMode::Sampled, + VoxelDensityProfile::DefaultSampleInterval); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeDensityProfile] game_mode=sampled sample_interval=%u"), + VoxelDensityProfile::DefaultSampleInterval); + } + if (!Settings) { UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] No Settings assigned — world won't generate.")); @@ -2379,10 +2424,24 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector double StreamSeconds = 0.0; int32 ClassifyVerdict = -1; // Mixed = 0, AllSolid = 1, AllAir = 2 FVoxelTileClassificationStats ClassifierStats; + const bool bProfileOps = bProfileTile + && VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full; + const VoxelDensityProfile::FSnapshot TileProfileStart = bProfileTile + ? VoxelDensityProfile::SnapshotCurrentThread() + : VoxelDensityProfile::FSnapshot(); auto EmitTileProfile = [&]() { if (bProfileTile) { + const VoxelDensityProfile::FSnapshot TileProfileEnd = bProfileTile + ? VoxelDensityProfile::SnapshotCurrentThread() + : VoxelDensityProfile::FSnapshot(); + const auto CounterDelta = [&](VoxelDensityProfile::ECounter Counter) -> uint64 + { + const int32 Index = static_cast(Counter); + return bProfileTile && TileProfileEnd.Counters[Index] >= TileProfileStart.Counters[Index] + ? TileProfileEnd.Counters[Index] - TileProfileStart.Counters[Index] : 0; + }; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d " "verdict=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f " @@ -2392,7 +2451,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector "room_work=%.6f room_exact=%.6f cache_work=%.6f " "rooms=%d tunnels=%d joins=%d pits=%d chimneys=%d " "whole_mixed=%u whole_solid=%u whole_air=%u final_nodes=%u " - "split_nodes=%u max_depth=%u"), + "split_nodes=%u max_depth=%u block_builds=%llu block_samples=%llu " + "block_ops=%llu block_active=%llu block_pruned=%llu"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells, bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0, Result.bEmpty ? 1 : 0, ClassifyVerdict, ClassifySeconds, MeshSeconds, StreamSeconds, @@ -2414,7 +2474,53 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector ClassifierStats.RoomNumChimneys, ClassifierStats.WholeMixedNodes, ClassifierStats.WholeSolidNodes, ClassifierStats.WholeAirNodes, ClassifierStats.NeedsFinalFieldNodes, - ClassifierStats.SplitNodes, ClassifierStats.MaxRefinementDepth); + ClassifierStats.SplitNodes, ClassifierStats.MaxRefinementDepth, + static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockBuilds)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockSamples)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockOperators)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockActiveOperators)), + static_cast(CounterDelta(VoxelDensityProfile::ECounter::OpBlockPrunedOperators))); + + if (bProfileOps) + { + static const VoxelDensityProfile::EBucket GameOpBuckets[] = { + VoxelDensityProfile::EBucket::RoomGraphSource, + VoxelDensityProfile::EBucket::SdfCarve, + VoxelDensityProfile::EBucket::CaveRoughnessMod, + VoxelDensityProfile::EBucket::CaveTerraceMod, + VoxelDensityProfile::EBucket::LayerLineMod, + VoxelDensityProfile::EBucket::RibbingMod, + VoxelDensityProfile::EBucket::CaveOverhangMod, + VoxelDensityProfile::EBucket::CaveCliffMod, + VoxelDensityProfile::EBucket::ScallopMod, + VoxelDensityProfile::EBucket::CaveArchMod, + VoxelDensityProfile::EBucket::RoomColumnMod, + VoxelDensityProfile::EBucket::DomeMod, + VoxelDensityProfile::EBucket::PinchMod, + VoxelDensityProfile::EBucket::FloorBiasMod, + VoxelDensityProfile::EBucket::WormFieldSource, + VoxelDensityProfile::EBucket::OriginSpineOp, + VoxelDensityProfile::EBucket::BoundarySealOp, + VoxelDensityProfile::EBucket::PassageCarveOp, + VoxelDensityProfile::EBucket::XYEdgeSealOp, + }; + for (const VoxelDensityProfile::EBucket Bucket : GameOpBuckets) + { + const int32 Index = static_cast(Bucket); + const uint64 Calls = TileProfileEnd.Calls[Index] >= TileProfileStart.Calls[Index] + ? TileProfileEnd.Calls[Index] - TileProfileStart.Calls[Index] : 0; + const uint64 Cycles = TileProfileEnd.WallCycles[Index] >= TileProfileStart.WallCycles[Index] + ? TileProfileEnd.WallCycles[Index] - TileProfileStart.WallCycles[Index] : 0; + if (Calls == 0) { continue; } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeGameOpProfile] tile=(%d,%d,%d) level=%d op=%s calls=%llu total_us=%.3f us_per_call=%.6f"), + Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, + VoxelDensityProfile::BucketName(Bucket), + static_cast(Calls), + FPlatformTime::ToSeconds64(Cycles) * 1.0e6, + FPlatformTime::ToSeconds64(Cycles) * 1.0e6 / static_cast(Calls)); + } + } } }; diff --git a/Source/VoxelForge/Public/VoxelDensityOp.h b/Source/VoxelForge/Public/VoxelDensityOp.h index 4b9b5e5..72e9b85 100644 --- a/Source/VoxelForge/Public/VoxelDensityOp.h +++ b/Source/VoxelForge/Public/VoxelDensityOp.h @@ -103,6 +103,7 @@ struct FBiomeContext; struct FTunnelCoreWorldEvaluation; +struct FChunkSDFCache; //============================================================================= // LES QUATRE RÔLES / THE FOUR ROLES @@ -341,6 +342,47 @@ struct FVoxelOpSample // with the sample keeps the rule true even when a detail op is unaware of the room graph. bool bProtectedStructuralFloor = false; float StructuralFloorMinimumDensity = 0.0f; + + // Block evaluation keeps the room source's per-sample selection explicit. The scalar path + // still uses the source's worker-local state, but a later block-capable modifier must never + // observe the state belonging to the last sample of the preceding operator. + const FChunkSDFCache* RoomCache = nullptr; + int32 NearestRoomIndex = INDEX_NONE; + + // The common generator tail consumes these values after the block has left the stack. They + // are the small, exact subset of FTunnelCoreWorldEvaluation that the tail needs; keeping them + // in the sample avoids re-running the world-space tunnel scan or consulting a last-sample TLS + // value after an op-major evaluation. + bool bHasTunnelCoreWorldEvaluation = false; + float TunnelCoreWorldSDF = FLT_MAX; + bool bTunnelCoreSupportFloor = false; + bool bTunnelCoreRoomFloor = false; +}; + +/** + * A regular world lattice handed to one operator at a time. + * + * The default implementation is deliberately scalar and therefore source-compatible with custom + * operators. An operator may override EvalBlock only when it preserves the exact per-sample + * semantics of Eval. Samples are laid out X-fastest, then Y, then Z; all coordinates are voxel + * coordinates and are formed as Origin + index * Step using the same integer arithmetic as the + * mesher's scalar path. + */ +struct FVoxelOpBlock +{ + FIntVector OriginVoxels = FIntVector::ZeroValue; + int32 Step = 1; + int32 SizeX = 0; + int32 SizeY = 0; + int32 SizeZ = 0; + FVoxelOpSample* Samples = nullptr; + + int32 NumSamples() const { return SizeX * SizeY * SizeZ; } + + FORCEINLINE FVoxelOpSample& At(int32 X, int32 Y, int32 Z) const + { + return Samples[(Z * SizeY + Y) * SizeX + X]; + } }; /** @@ -477,6 +519,42 @@ public: */ virtual void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const = 0; + /** + * Op-major evaluation seam. The conservative default is intentionally not clever: it calls + * the canonical scalar Eval for every sample. This lets the stack and mesher be converted in + * reviewable stages while byte-identical output remains the acceptance test. Stateful ops + * may restore their per-sample hand-off from FVoxelOpSample in PrepareBlockSample; the default + * invokes that hook before each scalar fallback call. + */ + virtual void EvalBlock(const FVoxelOpBlock& Block) const + { + if (Block.Samples == nullptr || Block.Step <= 0 + || Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0) + { + return; + } + + for (int32 Z = 0; Z < Block.SizeZ; ++Z) + { + for (int32 Y = 0; Y < Block.SizeY; ++Y) + { + for (int32 X = 0; X < Block.SizeX; ++X) + { + FVoxelOpSample& Sample = Block.At(X, Y, Z); + PrepareBlockSample(Sample); + Eval( + static_cast(Block.OriginVoxels.X + X * Block.Step), + static_cast(Block.OriginVoxels.Y + Y * Block.Step), + static_cast(Block.OriginVoxels.Z + Z * Block.Step), + Sample); + } + } + } + } + + /** Restore a source-owned per-sample context before a scalar fallback in EvalBlock. */ + virtual void PrepareBlockSample(const FVoxelOpSample&) const {} + /** Optional hand-off for the common structural tail. The default keeps custom operators * conservative: the caller uses its canonical cache path when no source publishes a result. */ virtual bool TryGetLastTunnelCoreWorldEvaluation( diff --git a/Source/VoxelForge/Public/VoxelDensityOpStack.h b/Source/VoxelForge/Public/VoxelDensityOpStack.h index 6b39b43..79c2f23 100644 --- a/Source/VoxelForge/Public/VoxelDensityOpStack.h +++ b/Source/VoxelForge/Public/VoxelDensityOpStack.h @@ -88,6 +88,7 @@ private: EVoxelOpResourceMask RequiredResources = VoxelOpResources::None; EVoxelOpResourceMask ProvidedResources = VoxelOpResources::None; const TCHAR* ProfileName = nullptr; + VoxelDensityProfile::EBucket ProfileBucket = VoxelDensityProfile::EBucket::OtherOp; }; public: @@ -125,6 +126,7 @@ public: Entry.RequiredResources = Entry.Op->RequiredResources(); Entry.ProvidedResources = Entry.Op->ProvidedResources(); Entry.ProfileName = Entry.Op->DebugName(); + Entry.ProfileBucket = VoxelDensityProfile::BucketFromName(Entry.ProfileName); } Ops.Add(MoveTemp(Entry)); } @@ -157,6 +159,8 @@ public: * mensonge utile qui finirait par masquer une course. */ void PrepareChunk(const FVoxelOpContext& Ctx) { + PreparedContext = Ctx; + bHasPreparedContext = true; for (const FOpEntry& Entry : Ops) { Entry.Op->PrepareChunk(Ctx); } } @@ -178,10 +182,21 @@ public: FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const { FVoxelOpSample S; + // Fine operation timers are a diagnostic mode. Keeping their construction out of the + // ordinary scalar path matters for callers that still use fractional samples or an + // archetype without a block session; the block path already times one scope per op/block. + const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full; for (const FOpEntry& Entry : Ops) { - VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileName); - Entry.Op->Eval(WorldX, WorldY, WorldZ, S); + if (bProfileOps) + { + VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket); + Entry.Op->Eval(WorldX, WorldY, WorldZ, S); + } + else + { + Entry.Op->Eval(WorldX, WorldY, WorldZ, S); + } } // The room-graph source publishes the authored floor ownership while evaluating the @@ -194,6 +209,118 @@ public: return S; } + /** + * Evaluate one regular lattice in operator order. The caller owns and initializes the sample + * array (normally to FVoxelOpSample defaults); each operator transforms that same array before + * the next operator runs. The timer is intentionally one scope per operator/block rather than + * one scope per operator/sample. Operators without a block implementation use the conservative + * scalar fallback supplied by IVoxelDensityOp, so this entry point is safe to introduce before + * converting the concrete operators. + */ + void EvalBlock(const FVoxelOpBlock& Block) const + { + if (Block.Samples == nullptr || Block.Step <= 0 + || Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0) + { + return; + } + + const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full; + for (const FOpEntry& Entry : Ops) + { + if (bProfileOps) + { + VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket); + Entry.Op->EvalBlock(Block); + } + else + { + Entry.Op->EvalBlock(Block); + } + } + } + + /** Evaluate only the entries that the interval fold proved non-identity for this block. */ + void EvalBlock(const FVoxelOpBlock& Block, const TArray& ActiveOps) const + { + if (Block.Samples == nullptr || Block.Step <= 0 + || Block.SizeX <= 0 || Block.SizeY <= 0 || Block.SizeZ <= 0) + { + return; + } + + const bool bProfileOps = VoxelDensityProfile::GetMode() == VoxelDensityProfile::EMode::Full; + for (const int32 OpIndex : ActiveOps) + { + if (!Ops.IsValidIndex(OpIndex)) { continue; } + const FOpEntry& Entry = Ops[OpIndex]; + if (bProfileOps) + { + VoxelDensityProfile::FScopedTimer ProfileTimer(Entry.ProfileBucket); + Entry.Op->EvalBlock(Block); + } + else + { + Entry.Op->EvalBlock(Block); + } + } + } + + /** + * Build a conservative active list for one exact mesher lattice block. An operator may be + * omitted only when its state-aware box contract says Identity and it does not publish SDF. + * SDF writers remain in the list even when their density effect is Identity: their SDF is a + * channel consumed by a later converter, and an unchanged-looking interval is not by itself a + * pointwise proof. Unknown costs the operator visit, never the geometry. + */ + int32 BuildActiveOpList(const FBox& VoxelBox, int32 InStep, + FIntVector InLatticeOrigin, TArray& OutActiveOps) const + { + OutActiveOps.Reset(); + if (!bHasPreparedContext) + { + for (int32 Index = 0; Index < Ops.Num(); ++Index) { OutActiveOps.Add(Index); } + return OutActiveOps.Num(); + } + + FVoxelOpContext Ctx = PreparedContext; + Ctx.Step = FMath::Max(InStep, 1); + Ctx.LatticeOriginVoxels = InLatticeOrigin; + Ctx.bUseLatticeProof = true; + + FVoxelBoxHypotheses H; + for (int32 Index = 0; Index < Ops.Num(); ++Index) + { + const FOpEntry& Entry = Ops[Index]; + const bool bWritesSdf = (Entry.Writes & VoxelOpChannels::Sdf) != 0; + const bool bPublishesResource = Entry.ProvidedResources != VoxelOpResources::None; + const EVoxelTileClass Forced = Entry.Op->ClassifyBox(VoxelBox, Ctx); + bool bCanSkip = false; + + if (Forced != EVoxelTileClass::Mixed) + { + VF_ForceHypotheses(H, Forced, Entry.Op->ForcedMarginOverBox(VoxelBox, Ctx)); + if (bWritesSdf) { Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); } + } + else + { + const EVoxelOpEffect Effect = + Entry.Op->EffectOverBox(VoxelBox, Ctx, H); + bCanSkip = Effect == EVoxelOpEffect::Identity + && !bWritesSdf + && !bPublishesResource; + VF_FoldEffect(H, + Effect, + Entry.Op->MaxCarveOverBox(VoxelBox, Ctx, H), + Entry.Op->MaxFillOverBox(VoxelBox, Ctx, H)); + if (bWritesSdf) { Entry.Op->PropagateSdfOverBox(H.Sdf, VoxelBox, Ctx); } + } + + if (!bCanSkip) { OutActiveOps.Add(Index); } + } + return OutActiveOps.Num(); + } + /** Le même, négaté pour le mesher (négatif = solide). */ float EvalMC(float WorldX, float WorldY, float WorldZ) const { @@ -346,6 +473,8 @@ public: private: TArray Ops; + FVoxelOpContext PreparedContext; + bool bHasPreparedContext = false; }; //============================================================================= diff --git a/Source/VoxelForge/Public/VoxelDensityProfile.h b/Source/VoxelForge/Public/VoxelDensityProfile.h index 74946c0..8e59543 100644 --- a/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -123,6 +123,11 @@ namespace VoxelDensityProfile TileVerdictCacheHits, TileVerdictRegionHits, TileVerdictCacheStores, + OpBlockBuilds, + OpBlockSamples, + OpBlockOperators, + OpBlockActiveOperators, + OpBlockPrunedOperators, Count }; @@ -231,6 +236,9 @@ namespace VoxelDensityProfile VOXELFORGE_API void CalibrateTimer(); VOXELFORGE_API void Reset(); VOXELFORGE_API FSnapshot Snapshot(); + // Worker-local snapshot for a tile profile. GenerateTileResult is single-owner on its worker, + // so this avoids attributing another worker's concurrent tile to the current tile diagnostic. + VOXELFORGE_API FSnapshot SnapshotCurrentThread(); VOXELFORGE_API void AddCounter(ECounter Counter, uint64 Amount = 1); VOXELFORGE_API void AddMeasurement(EBucket Bucket, uint64 Cycles, uint64 Calls = 1); VOXELFORGE_API void RecordCall(EBucket Bucket); diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index bfb35e6..5b73f6e 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -143,6 +143,19 @@ public: */ float GetDensityAt(float WorldX, float WorldY, float WorldZ) const; + /** + * Open a worker-local lattice hand-off for the following GetDensityAt calls. The mesher uses + * this around one tile's density-grid fill. The first sample in each resolved chunk lets the + * generator run the operator stack once in EvalBlock; later samples consume the exact cached + * per-sample result while the ordinary post-stack tail still runs in canonical order. + * + * This is intentionally a scoped service rather than a persistent world cache: its lifetime is + * one tile task, it is thread-local, and EndDensityBlock must be called on every exit path. + */ + void BeginDensityBlock(FIntVector OriginVoxels, int32 Step, + int32 SizeX, int32 SizeY, int32 SizeZ) const; + void EndDensityBlock() const; + /** * Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise). * Utilisée en interne par GetDensityAt quand la strate est de ce type.