Files
UnrealEngine/Engine/Source/Runtime/Renderer/Private/Nanite/NaniteFeedback.cpp
2025-05-18 13:04:45 +08:00

175 lines
6.7 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "NaniteFeedback.h"
#include "ScenePrivate.h"
#include "RendererModule.h"
#include "RendererOnScreenNotification.h"
#include "GPUMessaging.h"
#define LOCTEXT_NAMESPACE "NaniteFeedbackStatus"
static TAutoConsoleVariable<int32> CVarEmitMaterialPerformanceWarnings(
TEXT("r.Nanite.EmitMaterialPerformanceWarnings"),
0,
TEXT("Emit log and on-screen messages to warn when a Nanite material is both programmable and using either masking or pixel depth offset (PDO)."),
ECVF_RenderThreadSafe
);
namespace Nanite
{
#if !UE_BUILD_SHIPPING
FFeedbackManager::FFeedbackManager()
{
StatusFeedbackSocket = GPUMessage::RegisterHandler(TEXT("Nanite.StatusFeedback"),
[this](GPUMessage::FReader Message)
{
const uint32 MaxNodes = Nanite::FGlobalResources::GetMaxNodes();
const uint32 MaxCandidateClusters = Nanite::FGlobalResources::GetMaxCandidateClusters();
const uint32 MaxVisibleClusters = Nanite::FGlobalResources::GetMaxVisibleClusters();
const uint32 PeakNodes = Message.Read<uint32>(0);
const uint32 PeakCandidateClusters = Message.Read<uint32>(0);
const uint32 PeakVisibleClusters = Message.Read<uint32>(0);
if (NodeState.Update(PeakNodes, MaxNodes))
{
UE_LOG(LogRenderer, Warning, TEXT( "Nanite node buffer overflow detected. New high-water mark is %d / %d. "
"Increase r.Nanite.MaxNodes to prevent potential visual artifacts."), NodeState.HighWaterMark, MaxNodes);
}
if (CandidateClusterState.Update(PeakCandidateClusters, MaxCandidateClusters))
{
UE_LOG(LogRenderer, Warning, TEXT( "Nanite candidate cluster buffer overflow detected. New high-water mark is %d / %d. "
"Increase r.Nanite.MaxCandidateClusters to prevent potential visual artifacts."), CandidateClusterState.HighWaterMark, MaxCandidateClusters);
}
if (VisibleClusterState.Update(PeakVisibleClusters, MaxVisibleClusters))
{
UE_LOG(LogRenderer, Warning, TEXT( "Nanite visible cluster buffer overflow detected. New high-water mark is %d / %d. "
"Increase r.Nanite.MaxVisibleClusters to prevent potential visual artifacts."), VisibleClusterState.HighWaterMark, MaxVisibleClusters);
}
});
// TODO: Should be FRendererOnScreenNotification::Get(). Temporary workaround for singleton initialization issue.
// The problem is that the FFeedbackManager lives inside Nanite::FGlobalResources which is a global resource, released by the system after the
// FRendererOnScreenNotification singleton is destroyed.
// WARNING: FCoreDelegates::OnGetOnScreenMessages is invoked from the Game Thread!
ScreenMessageDelegate = FCoreDelegates::OnGetOnScreenMessages.AddLambda(
[this](TMultiMap<FCoreDelegates::EOnScreenMessageSeverity, FText>& OutMessages)
{
const uint32 MaxNodes = Nanite::FGlobalResources::GetMaxNodes();
const uint32 MaxCandidateClusters = Nanite::FGlobalResources::GetMaxCandidateClusters();
const uint32 MaxVisibleClusters = Nanite::FGlobalResources::GetMaxVisibleClusters();
const double ShowWarningSeconds = 5.0;
const double CurrentTime = FPlatformTime::Seconds();
if (CurrentTime - NodeState.LatestOverflowTime < ShowWarningSeconds)
{
OutMessages.Add(FCoreDelegates::EOnScreenMessageSeverity::Warning,
FText::Format(LOCTEXT("NaniteNodeOverflow",
"Nanite node buffer overflow detected: {0} / {1}. High-water mark is {2}. "
"Increase r.Nanite.MaxNodes to prevent potential visual artifacts."),
NodeState.LatestOverflowPeak, MaxNodes, NodeState.HighWaterMark));
}
if (CurrentTime - CandidateClusterState.LatestOverflowTime < ShowWarningSeconds)
{
OutMessages.Add(FCoreDelegates::EOnScreenMessageSeverity::Warning,
FText::Format(LOCTEXT("NaniteCandidateClusterOverflow",
"Nanite candidate cluster buffer overflow detected: {0} / {1}. High-water mark is {2}. "
"Increase r.Nanite.MaxCandidateClusters to prevent potential visual artifacts."),
CandidateClusterState.LatestOverflowPeak, MaxCandidateClusters, CandidateClusterState.HighWaterMark));
}
if (CurrentTime - VisibleClusterState.LatestOverflowTime < ShowWarningSeconds)
{
OutMessages.Add(FCoreDelegates::EOnScreenMessageSeverity::Warning,
FText::Format(LOCTEXT("NaniteVisibleClusterOverflow",
"Nanite visible cluster buffer overflow detected: {0} / {1}. High-water mark is {2}. "
"Increase r.Nanite.MaxVisibleClusters to prevent potential visual artifacts."),
VisibleClusterState.LatestOverflowPeak, MaxVisibleClusters, VisibleClusterState.HighWaterMark));
}
if (CVarEmitMaterialPerformanceWarnings.GetValueOnAnyThread() != 0)
{
FScopeLock Lock(&DelgateCallbackCS);
for (const auto& Item : MaterialWarningItems)
{
if (CurrentTime - Item.Value.LastTimeSeen < 2.5f)
{
OutMessages.Add(FCoreDelegates::EOnScreenMessageSeverity::Warning, FText::FromString(FString::Printf(TEXT("Performance Warning: Programmable Nanite material '%s' uses PDO or is Masked!"), *Item.Key)));
}
}
// Strip old material warning items
MaterialWarningItems = MaterialWarningItems.FilterByPredicate([CurrentTime](const TMap<FString, FMaterialWarningItem>::ElementType& Element)
{
return CurrentTime - Element.Value.LastTimeSeen < 5.0f;
});
}
else
{
FScopeLock Lock(&DelgateCallbackCS);
MaterialWarningItems.Empty();
}
});
}
FFeedbackManager::~FFeedbackManager()
{
FCoreDelegates::OnGetOnScreenMessages.Remove(ScreenMessageDelegate); // TODO: Should be FRendererOnScreenNotification::Get(). Temporary workaround for singleton initialization issue.
}
bool FFeedbackManager::FBufferState::Update(const uint32 Peak, const uint32 Capacity)
{
bool bNewHighWaterMark = false;
if (Peak > Capacity)
{
LatestOverflowTime = FPlatformTime::Seconds();
LatestOverflowPeak = Peak;
bNewHighWaterMark = (Peak > HighWaterMark);
}
HighWaterMark = FMath::Max(HighWaterMark, Peak);
return bNewHighWaterMark;
}
void FFeedbackManager::ReportMaterialPerformanceWarning(const FString &MaterialName)
{
bool bShouldLogNow = false;
{
FScopeLock Lock(&DelgateCallbackCS);
FMaterialWarningItem& Item = MaterialWarningItems.FindOrAdd(MaterialName);
const double CurrentTime = FPlatformTime::Seconds();
bShouldLogNow = CurrentTime - Item.LastTimeLogged > 5.0f;
Item.LastTimeSeen = CurrentTime;
if (bShouldLogNow)
{
Item.LastTimeLogged = CurrentTime;
}
}
// Keep logging outside critical section
if (bShouldLogNow)
{
UE_LOG(LogRenderer, Log, TEXT("Performance Warning: Programmable Nanite material uses PDO or is Masked, %s"), *MaterialName);
}
}
bool ShouldReportFeedbackMaterialPerformanceWarning()
{
return CVarEmitMaterialPerformanceWarnings.GetValueOnRenderThread() != 0;
}
#endif // !UE_BUILD_SHIPPING
} // namespace Nanite
#undef LOCTEXT_NAMESPACE