// Copyright Ngonart OU. All Rights Reserved. // UE5 Modular Game Framework — SS_PlanarCaptureManager implementation #include "Capture/SS_PlanarCaptureManager.h" #include "Capture/BP_PlanarCaptureActor.h" #include "Capture/BPC_PlanarCapture.h" #include "Engine/TextureRenderTarget2D.h" #include "Engine/World.h" #include "GameFramework/PlayerController.h" #include "Kismet/GameplayStatics.h" USS_PlanarCaptureManager::USS_PlanarCaptureManager() { } void USS_PlanarCaptureManager::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Initialized for world.")); } void USS_PlanarCaptureManager::Deinitialize() { // Release all render targets for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool) { if (Entry.RenderTarget) { Entry.RenderTarget->ConditionalBeginDestroy(); } } RenderTargetPool.Empty(); RegisteredSurfaces.Empty(); Super::Deinitialize(); } void USS_PlanarCaptureManager::Tick(float DeltaTime) { TimeSinceLastEvaluation += DeltaTime; if (TimeSinceLastEvaluation >= FullEvaluationInterval) { TimeSinceLastEvaluation = 0.0f; EvaluateAllSurfaces(); } } TStatId USS_PlanarCaptureManager::GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(USS_PlanarCaptureManager, STATGROUP_Tickables); } // ======================================================================== // Surface Registry // ======================================================================== void USS_PlanarCaptureManager::RegisterSurface(ABP_PlanarCaptureActor* Surface) { if (!Surface) { return; } // Check for duplicates for (const TWeakObjectPtr& Existing : RegisteredSurfaces) { if (Existing.Get() == Surface) { UE_LOG(LogTemp, Warning, TEXT("SS_PlanarCaptureManager: Surface '%s' already registered."), *Surface->SurfaceDisplayName); return; } } RegisteredSurfaces.Add(Surface); UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Registered surface '%s'. Total: %d"), *Surface->SurfaceDisplayName, RegisteredSurfaces.Num()); OnSurfaceRegistered.Broadcast(Surface, RegisteredSurfaces.Num()); // Evaluate immediately to assign initial tier EvaluateAllSurfaces(); } void USS_PlanarCaptureManager::UnregisterSurface(ABP_PlanarCaptureActor* Surface) { if (!Surface) { return; } RegisteredSurfaces.RemoveAll([Surface](const TWeakObjectPtr& Entry) { return Entry.Get() == Surface; }); UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Unregistered surface '%s'. Total: %d"), *Surface->SurfaceDisplayName, RegisteredSurfaces.Num()); OnSurfaceUnregistered.Broadcast(Surface, RegisteredSurfaces.Num()); } TArray USS_PlanarCaptureManager::GetRegisteredSurfaces() const { TArray Result; for (const TWeakObjectPtr& Entry : RegisteredSurfaces) { if (Entry.IsValid()) { Result.Add(Entry.Get()); } } return Result; } // ======================================================================== // Quality Budget Management // ======================================================================== void USS_PlanarCaptureManager::ForceAllSurfacesToTier(EPlanarCaptureQualityTier Tier) { ForceTierOverride = Tier; for (TWeakObjectPtr& SurfaceWeak : RegisteredSurfaces) { if (ABP_PlanarCaptureActor* Surface = SurfaceWeak.Get()) { if (UBPC_PlanarCapture* Capture = Surface->CaptureComponent) { Capture->ApplyQualityTier(Tier); } } } } void USS_PlanarCaptureManager::ReleaseForceTier() { ForceTierOverride.Reset(); EvaluateAllSurfaces(); } // ======================================================================== // Render Target Pool // ======================================================================== UTextureRenderTarget2D* USS_PlanarCaptureManager::RequestRenderTarget(int32 Size) { // Check pool for an available RT of the right size for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool) { if (!Entry.bInUse && Entry.CurrentSize == Size && Entry.RenderTarget) { Entry.bInUse = true; return Entry.RenderTarget; } } // Create a new one return CreateRenderTarget(Size); } void USS_PlanarCaptureManager::ReleaseRenderTarget(UTextureRenderTarget2D* RenderTarget) { if (!RenderTarget) { return; } // Find the entry and mark as free for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool) { if (Entry.RenderTarget == RenderTarget) { Entry.bInUse = false; Entry.OwningSurface.Reset(); return; } } // Not in pool — add it FPlanarCaptureRenderTargetEntry NewEntry; NewEntry.RenderTarget = RenderTarget; NewEntry.CurrentSize = RenderTarget->SizeX; NewEntry.bInUse = false; RenderTargetPool.Add(NewEntry); } float USS_PlanarCaptureManager::GetPoolMemoryUsageMB() const { float TotalBytes = 0.0f; for (const FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool) { if (Entry.RenderTarget) { // RGBA8 = 4 bytes per pixel TotalBytes += static_cast(Entry.CurrentSize * Entry.CurrentSize * 4); } } return TotalBytes / (1024.0f * 1024.0f); } // ======================================================================== // Query // ======================================================================== ABP_PlanarCaptureActor* USS_PlanarCaptureManager::GetNearestSurfaceOfMode( EPlanarCaptureMode Mode, FVector WorldLocation, float MaxDistance) const { ABP_PlanarCaptureActor* Nearest = nullptr; float NearestDistSq = (MaxDistance > 0.0f) ? (MaxDistance * MaxDistance) : FLT_MAX; for (const TWeakObjectPtr& Entry : RegisteredSurfaces) { if (ABP_PlanarCaptureActor* Surface = Entry.Get()) { if (!Surface->CaptureComponent || Surface->CaptureComponent->CaptureMode != Mode) { continue; } const float DistSq = FVector::DistSquared(Surface->GetActorLocation(), WorldLocation); if (DistSq < NearestDistSq) { NearestDistSq = DistSq; Nearest = Surface; } } } return Nearest; } // ======================================================================== // Internal Methods // ======================================================================== void USS_PlanarCaptureManager::EvaluateAllSurfaces() { // Reset tier counts TierAssignmentCounts.Empty(); // Ensure registered surfaces are still valid RegisteredSurfaces.RemoveAll([](const TWeakObjectPtr& Entry) { return !Entry.IsValid(); }); // If force tier override is active, apply it to all if (ForceTierOverride.IsSet()) { ForceAllSurfacesToTier(ForceTierOverride.GetValue()); return; } // Score and assign tiers TArray> ScoredSurfaces; for (TWeakObjectPtr& SurfaceWeak : RegisteredSurfaces) { if (ABP_PlanarCaptureActor* Surface = SurfaceWeak.Get()) { if (!Surface->bIsActive || !Surface->CaptureComponent) { continue; } UBPC_PlanarCapture* Capture = Surface->CaptureComponent; FPlanarCaptureScore Score = Capture->GetCurrentScore(); ScoredSurfaces.Add(TPair(Surface, Score.CompositeScore)); } } // Sort by score descending (highest score first) ScoredSurfaces.Sort([](const TPair& A, const TPair& B) { return A.Value > B.Value; }); // Assign tiers within budget for (const auto& Pair : ScoredSurfaces) { ABP_PlanarCaptureActor* Surface = Pair.Key; float Score = Pair.Value; if (!Surface->CaptureComponent) { continue; } EPlanarCaptureQualityTier AssignedTier = ScoreAndAssignTier(Surface->CaptureComponent); // Apply tier Surface->CaptureComponent->ApplyQualityTier(AssignedTier); } } EPlanarCaptureQualityTier USS_PlanarCaptureManager::ScoreAndAssignTier(UBPC_PlanarCapture* Capture) { if (!Capture) { return EPlanarCaptureQualityTier::Off; } const FPlanarCaptureScore Score = Capture->GetCurrentScore(); // If not visible, off if (!Score.bInFrustum && Score.CompositeScore < 0.01f) { return EPlanarCaptureQualityTier::Off; } // If too far, off if (Score.DistanceToViewer > MaxCaptureDistance) { return EPlanarCaptureQualityTier::Off; } // Determine natural tier based on composite score EPlanarCaptureQualityTier NaturalTier; if (Score.CompositeScore >= 0.8f) { NaturalTier = EPlanarCaptureQualityTier::Hero; } else if (Score.CompositeScore >= 0.5f) { NaturalTier = EPlanarCaptureQualityTier::High; } else if (Score.CompositeScore >= 0.2f) { NaturalTier = EPlanarCaptureQualityTier::Medium; } else { NaturalTier = EPlanarCaptureQualityTier::Low; } // Apply global quality cap if (static_cast(NaturalTier) > static_cast(GlobalQualityCap)) { NaturalTier = GlobalQualityCap; } // Check budget limits const int32 CurrentCount = TierAssignmentCounts.FindRef(NaturalTier); switch (NaturalTier) { case EPlanarCaptureQualityTier::Hero: if (CurrentCount >= MaxHeroSurfaces) { NaturalTier = EPlanarCaptureQualityTier::High; } break; case EPlanarCaptureQualityTier::High: if (CurrentCount >= MaxHighSurfaces) { NaturalTier = EPlanarCaptureQualityTier::Medium; } break; case EPlanarCaptureQualityTier::Medium: if (CurrentCount >= MaxMediumSurfaces) { NaturalTier = EPlanarCaptureQualityTier::Low; } break; default: break; } // Increment the assigned tier count TierAssignmentCounts.FindOrAdd(NaturalTier)++; return NaturalTier; } void USS_PlanarCaptureManager::EnforceBudgetLimits() { // Additional enforcement for total memory budget float TotalMemoryMB = GetPoolMemoryUsageMB(); if (TotalMemoryMB > MaxTotalRenderTargetMemoryMB) { UE_LOG(LogTemp, Warning, TEXT("SS_PlanarCaptureManager: Render target memory budget exceeded (%.2f MB / %.2f MB)"), TotalMemoryMB, MaxTotalRenderTargetMemoryMB); // If over budget, demote lowest-priority surfaces // Future: implement more sophisticated memory budget enforcement } } UTextureRenderTarget2D* USS_PlanarCaptureManager::CreateRenderTarget(int32 Size) { UTextureRenderTarget2D* RT = NewObject(this); if (!RT) { return nullptr; } RT->InitCustomFormat(Size, Size, PF_B8G8R8A8, false); RT->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8; RT->bGPUSharedFlag = false; RT->ClearColor = FLinearColor::Black; RT->UpdateResourceImmediate(true); FPlanarCaptureRenderTargetEntry Entry; Entry.RenderTarget = RT; Entry.CurrentSize = Size; Entry.bInUse = true; RenderTargetPool.Add(Entry); return RT; } UTextureRenderTarget2D* USS_PlanarCaptureManager::GetOrCreateRenderTarget(int32 Size) { // Check pool first for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool) { if (!Entry.bInUse && Entry.CurrentSize == Size && Entry.RenderTarget) { Entry.bInUse = true; return Entry.RenderTarget; } } return CreateRenderTarget(Size); }