1716 lines
50 KiB
C++
1716 lines
50 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "RewindDebugger.h"
|
|
|
|
#include "Animation/AnimBlueprintGeneratedClass.h"
|
|
#include "Animation/AnimTrace.h"
|
|
#include "DesktopPlatformModule.h"
|
|
#include "Editor.h"
|
|
#include "Editor/UnrealEdEngine.h"
|
|
#include "Engine/World.h"
|
|
#include "EngineUtils.h"
|
|
#include "GameFramework/Pawn.h"
|
|
#include "HAL/PlatformFileManager.h"
|
|
#include "IAnimationProvider.h"
|
|
#include "IDesktopPlatform.h"
|
|
#include "IGameplayProvider.h"
|
|
#include "Insights/IUnrealInsightsModule.h"
|
|
#include "IRewindDebuggerDoubleClickHandler.h"
|
|
#include "IRewindDebuggerExtension.h"
|
|
#include "Kismet2/DebuggerCommands.h"
|
|
#include "LevelEditor.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "ObjectTrace.h"
|
|
#include "ProfilingDebugging/TraceAuxiliary.h"
|
|
#include "RewindDebuggerCommands.h"
|
|
#include "RewindDebuggerModule.h"
|
|
#include "RewindDebuggerObjectTrack.h"
|
|
#include "RewindDebuggerPlaceholderTrack.h"
|
|
#include "RewindDebuggerRuntime/RewindDebuggerRuntime.h"
|
|
#include "RewindDebuggerSettings.h"
|
|
#include "RewindDebuggerTrackCreators.h"
|
|
#include "SLevelViewport.h"
|
|
#include "SModalSessionBrowser.h"
|
|
#include "ToolMenus.h"
|
|
#include "Trace/StoreClient.h"
|
|
#include "TraceServices/Model/Frames.h"
|
|
#include "UnrealEdGlobals.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "Widgets/Docking/SDockTab.h"
|
|
#include "Widgets/Input/SNumericEntryBox.h"
|
|
#include "Widgets/Layout/SSpacer.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "RewindDebugger"
|
|
|
|
static void IterateExtensions(TFunction<void(IRewindDebuggerExtension* Extension)> IteratorFunction)
|
|
{
|
|
// update extensions
|
|
IModularFeatures& ModularFeatures = IModularFeatures::Get();
|
|
|
|
const int32 NumExtensions = ModularFeatures.GetModularFeatureImplementationCount(IRewindDebuggerExtension::ModularFeatureName);
|
|
for (int32 ExtensionIndex = 0; ExtensionIndex < NumExtensions; ++ExtensionIndex)
|
|
{
|
|
IRewindDebuggerExtension* Extension = static_cast<IRewindDebuggerExtension*>(ModularFeatures.GetModularFeatureImplementation(IRewindDebuggerExtension::ModularFeatureName, ExtensionIndex));
|
|
IteratorFunction(Extension);
|
|
}
|
|
}
|
|
|
|
static void TraceSubobjects(const UObject* OuterObject)
|
|
{
|
|
TArray<UObject*> Subobjects;
|
|
GetObjectsWithOuter(OuterObject, Subobjects, true);
|
|
for (const UObject* Subobject : Subobjects)
|
|
{
|
|
TRACE_OBJECT_LIFETIME_BEGIN(Subobject);
|
|
}
|
|
}
|
|
|
|
FRewindDebugger::FRewindDebugger()
|
|
{
|
|
if (RewindDebugger::FRewindDebuggerRuntime::Instance() == nullptr)
|
|
{
|
|
RewindDebugger::FRewindDebuggerRuntime::Initialize();
|
|
}
|
|
|
|
if (RewindDebugger::FRewindDebuggerRuntime* Runtime = RewindDebugger::FRewindDebuggerRuntime::Instance())
|
|
{
|
|
Runtime->ClearRecording.AddRaw(this, &FRewindDebugger::OnClearRecording);
|
|
Runtime->RecordingStarted.AddRaw(this, &FRewindDebugger::OnRecordingStarted);
|
|
Runtime->RecordingStarted.AddRaw(this, &FRewindDebugger::OnRecordingStopped);
|
|
}
|
|
|
|
RewindDebugger::FRewindDebuggerTrackCreators::EnumerateCreators([this](const RewindDebugger::IRewindDebuggerTrackCreator* Creator)
|
|
{
|
|
Creator->GetTrackTypes(TrackTypes);
|
|
});
|
|
|
|
RecordingDuration.Set(0);
|
|
|
|
UnrealInsightsModule = &FModuleManager::LoadModuleChecked<IUnrealInsightsModule>("TraceInsights");
|
|
|
|
if (GEditor->bIsSimulatingInEditor || GEditor->PlayWorld)
|
|
{
|
|
OnPIEStarted(true);
|
|
}
|
|
|
|
FEditorDelegates::PreBeginPIE.AddRaw(this, &FRewindDebugger::OnPIEStarted);
|
|
FEditorDelegates::PausePIE.AddRaw(this, &FRewindDebugger::OnPIEPaused);
|
|
FEditorDelegates::ResumePIE.AddRaw(this, &FRewindDebugger::OnPIEResumed);
|
|
FEditorDelegates::EndPIE.AddRaw(this, &FRewindDebugger::OnPIEStopped);
|
|
FEditorDelegates::SingleStepPIE.AddRaw(this, &FRewindDebugger::OnPIESingleStepped);
|
|
|
|
SelectedObjectName.OnPropertyChanged = SelectedObjectName.OnPropertyChanged.CreateLambda([this](FString Target)
|
|
{
|
|
URewindDebuggerSettings& Settings = URewindDebuggerSettings::Get();
|
|
if (Settings.DebugTargetActor != Target)
|
|
{
|
|
Settings.DebugTargetActor = Target;
|
|
Settings.Modify();
|
|
Settings.SaveConfig();
|
|
}
|
|
|
|
CandidateIds.SetNum(0);
|
|
GetTargetObjectIds(CandidateIds);
|
|
// make sure all the SubObjects of the target actor have been traced
|
|
#if OBJECT_TRACE_ENABLED
|
|
for (const RewindDebugger::FObjectId& TargetObjectId : CandidateIds)
|
|
{
|
|
if (const UObject* TargetObject = FObjectTrace::GetObjectFromId(TargetObjectId.GetMainId()))
|
|
{
|
|
TraceSubobjects(TargetObject);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
RefreshDebugTracks();
|
|
});
|
|
|
|
TickerHandle = FTSTicker::GetCoreTicker().AddTicker(TEXT("RewindDebugger"), 0.0f, [this](float DeltaTime)
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FRewindDebuggerModule_Tick);
|
|
|
|
Tick(DeltaTime);
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
FRewindDebugger::~FRewindDebugger()
|
|
{
|
|
FEditorDelegates::PostPIEStarted.RemoveAll(this);
|
|
FEditorDelegates::PausePIE.RemoveAll(this);
|
|
FEditorDelegates::ResumePIE.RemoveAll(this);
|
|
FEditorDelegates::EndPIE.RemoveAll(this);
|
|
FEditorDelegates::SingleStepPIE.RemoveAll(this);
|
|
|
|
FTSTicker::GetCoreTicker().RemoveTicker(TickerHandle);
|
|
|
|
if (RewindDebugger::FRewindDebuggerRuntime* Runtime = RewindDebugger::FRewindDebuggerRuntime::Instance())
|
|
{
|
|
Runtime->RecordingStarted.RemoveAll(this);
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::Initialize()
|
|
{
|
|
InternalInstance = new FRewindDebugger;
|
|
}
|
|
|
|
void FRewindDebugger::Shutdown()
|
|
{
|
|
delete InternalInstance;
|
|
}
|
|
|
|
void FRewindDebugger::SetTrackListChangedDelegate(const FOnTrackListChanged& InTrackListChangedDelegate)
|
|
{
|
|
TrackListChangedDelegate = InTrackListChangedDelegate;
|
|
}
|
|
|
|
void FRewindDebugger::SetTrackCursorDelegate(const FOnTrackCursor& InTrackCursorDelegate)
|
|
{
|
|
TrackCursorDelegate = InTrackCursorDelegate;
|
|
}
|
|
|
|
void FRewindDebugger::OnPIEStarted(bool bSimulating)
|
|
{
|
|
bPIEStarted = true;
|
|
bPIESimulating = true;
|
|
|
|
if (ShouldAutoRecordOnPIE())
|
|
{
|
|
bQueueStartRecording = true;
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::OnPIEPaused(bool bSimulating)
|
|
{
|
|
bPIESimulating = false;
|
|
ControlState = EControlState::Pause;
|
|
|
|
#if OBJECT_TRACE_ENABLED
|
|
if (IsRecording())
|
|
{
|
|
const UWorld* World = GetWorldToVisualize();
|
|
SetCurrentScrubTime(FObjectTrace::GetWorldElapsedTime(World));
|
|
}
|
|
#endif // OBJECT_TRACE_ENABLED
|
|
|
|
if (ShouldAutoEject() && FPlayWorldCommandCallbacks::IsInPIE())
|
|
{
|
|
bool CanEject = false;
|
|
for (auto It = GUnrealEd->SlatePlayInEditorMap.CreateIterator(); It; ++It)
|
|
{
|
|
CanEject = CanEject || It.Value().DestinationSlateViewport.IsValid();
|
|
}
|
|
|
|
if (CanEject)
|
|
{
|
|
GEditor->RequestToggleBetweenPIEandSIE();
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::OnPIEResumed(bool bSimulating)
|
|
{
|
|
bPIESimulating = true;
|
|
|
|
if (ShouldAutoEject() && FPlayWorldCommandCallbacks::IsInSIE())
|
|
{
|
|
GEditor->RequestToggleBetweenPIEandSIE();
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::OnPIESingleStepped(bool bSimulating)
|
|
{
|
|
#if OBJECT_TRACE_ENABLED
|
|
if (IsRecording())
|
|
{
|
|
const UWorld* World = GetWorldToVisualize();
|
|
SetCurrentScrubTime(FObjectTrace::GetWorldElapsedTime(World));
|
|
}
|
|
#endif // OBJECT_TRACE_ENABLED
|
|
}
|
|
|
|
|
|
void FRewindDebugger::OnPIEStopped(bool bSimulating)
|
|
{
|
|
if (IsRecording() && bPIESimulating)
|
|
{
|
|
#if OBJECT_TRACE_ENABLED
|
|
const UWorld* World = GetWorldToVisualize();
|
|
SetCurrentScrubTime(FObjectTrace::GetWorldElapsedTime(World));
|
|
#endif // OBJECT_TRACE_ENABLED
|
|
}
|
|
|
|
bPIEStarted = false;
|
|
bPIESimulating = false;
|
|
|
|
StopRecording();
|
|
|
|
bDisplayWorldIdValid = false;
|
|
}
|
|
|
|
bool FRewindDebugger::GetTargetActorPosition(FVector& OutPosition) const
|
|
{
|
|
OutPosition = TargetActorPosition;
|
|
return bTargetActorPositionValid;
|
|
}
|
|
|
|
uint64 FRewindDebugger::GetTargetActorId() const
|
|
{
|
|
if (SelectedObjectName.Get() == "")
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
uint64 TargetActorId = 0;
|
|
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
if (const IGameplayProvider* GameplayProvider = Session->ReadProvider<IGameplayProvider>("GameplayProvider"))
|
|
{
|
|
GameplayProvider->EnumerateObjects(CurrentTraceRange.GetLowerBoundValue(), CurrentTraceRange.GetUpperBoundValue(), [this, &TargetActorId](const FObjectInfo& InObjectInfo)
|
|
{
|
|
if (SelectedObjectName.Get() == InObjectInfo.Name)
|
|
{
|
|
TargetActorId = InObjectInfo.GetUObjectId();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return TargetActorId;
|
|
}
|
|
|
|
void FRewindDebugger::GetTargetObjectIds(TArray<RewindDebugger::FObjectId>& OutTargetObjectIds) const
|
|
{
|
|
OutTargetObjectIds.Empty(2);
|
|
|
|
if (SelectedObjectName.Get() == "")
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
if (const IGameplayProvider* GameplayProvider = Session->ReadProvider<IGameplayProvider>("GameplayProvider"))
|
|
{
|
|
GameplayProvider->EnumerateObjects(CurrentTraceRange.GetLowerBoundValue(), CurrentTraceRange.GetUpperBoundValue(), [this, &OutTargetObjectIds](const FObjectInfo& InObjectInfo)
|
|
{
|
|
// Skip children
|
|
if (!InObjectInfo.GetId().IsChildElement() && SelectedObjectName.Get() == InObjectInfo.Name)
|
|
{
|
|
OutTargetObjectIds.Add(InObjectInfo.GetId());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// make sure all the SubObjects of the target actor have been traced
|
|
#if OBJECT_TRACE_ENABLED
|
|
if (IsRecording())
|
|
{
|
|
for (const RewindDebugger::FObjectId& CandidateId : CandidateIds)
|
|
{
|
|
if (const UObject* TargetObject = FObjectTrace::GetObjectFromId(CandidateId.GetMainId()))
|
|
{
|
|
TraceSubobjects(TargetObject);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void FRewindDebugger::RefreshDebugTracks()
|
|
{
|
|
static const FName DebugMessageTrackName = "DebugMessageDummyTrack";
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FRewindDebugger::RefreshDebugTracks);
|
|
|
|
if (CandidateIds.Num() == 0)
|
|
{
|
|
GetTargetObjectIds(CandidateIds);
|
|
}
|
|
|
|
const FString SelectionName = SelectedObjectName.Get();
|
|
|
|
if (CandidateIds.Num() == 0 && !SelectionName.IsEmpty())
|
|
{
|
|
// fallback code path for when the target object is not found
|
|
|
|
if (DebugTracks.Num() != 2)
|
|
{
|
|
// clear tracks so we don't show data from previous recordings
|
|
DebugTracks.SetNum(0);
|
|
DebugTracks.SetNum(2);
|
|
}
|
|
|
|
if (DebugTracks[1] == nullptr || DebugTracks[0] == nullptr || DebugTracks[0]->GetName().ToString() != SelectionName)
|
|
{
|
|
DebugTracks[0] = MakeShared<FRewindDebuggerPlaceholderTrack>(FName(SelectionName), FText::FromString(SelectionName));
|
|
DebugTracks[1] = MakeShared<FRewindDebuggerPlaceholderTrack>(DebugMessageTrackName, NSLOCTEXT("RewindDebugger", "No Debug Data", " - Start a recording to debug"));
|
|
TrackListChangedDelegate.ExecuteIfBound();
|
|
}
|
|
}
|
|
else if (DebugTracks.Num() || CandidateIds.Num())
|
|
{
|
|
bool bChanged = false;
|
|
|
|
// remove any existing tracks that don't match the current list of object ids
|
|
for (int TrackIndex = DebugTracks.Num() - 1; TrackIndex >= 0; TrackIndex--)
|
|
{
|
|
if (!CandidateIds.Contains(DebugTracks[TrackIndex]->GetAssociatedObjectId()))
|
|
{
|
|
DebugTracks.RemoveAt(TrackIndex);
|
|
}
|
|
}
|
|
|
|
// add new tracks for current list of object identifiers if they don't already exist
|
|
for (const RewindDebugger::FObjectId& CandidateIdentifier : CandidateIds)
|
|
{
|
|
const TSharedPtr<RewindDebugger::FRewindDebuggerTrack>* FoundTrack = DebugTracks.FindByPredicate(
|
|
[CandidateIdentifier](const TSharedPtr<RewindDebugger::FRewindDebuggerTrack>& Track)
|
|
{
|
|
return Track->GetAssociatedObjectId() == CandidateIdentifier;
|
|
});
|
|
|
|
if (!FoundTrack)
|
|
{
|
|
DebugTracks.Add(MakeShared<RewindDebugger::FRewindDebuggerObjectTrack>(CandidateIdentifier, SelectedObjectName.Get(), true));
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
// update all tracks
|
|
for (const TSharedPtr<RewindDebugger::FRewindDebuggerTrack>& DebugTrack : DebugTracks)
|
|
{
|
|
if (DebugTrack->Update())
|
|
{
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
if (bChanged)
|
|
{
|
|
TrackListChangedDelegate.ExecuteIfBound();
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::OnConnection()
|
|
{
|
|
// queue up some operations to happen on the game thread next tick
|
|
bTraceJustConnected = true;
|
|
FTraceAuxiliary::OnConnection.RemoveAll(this);
|
|
}
|
|
|
|
void FRewindDebugger::StartRecording() const
|
|
{
|
|
if (!CanStartRecording())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (RewindDebugger::FRewindDebuggerRuntime* Runtime = RewindDebugger::FRewindDebuggerRuntime::Instance())
|
|
{
|
|
Runtime->StartRecording();
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::OnClearRecording()
|
|
{
|
|
ClearTrace();
|
|
RecordingDuration.Set(0);
|
|
CandidateIds.Empty(2);
|
|
bTargetActorPositionValid = false;
|
|
|
|
IterateExtensions([this](IRewindDebuggerExtension* Extension)
|
|
{
|
|
Extension->Clear(this);
|
|
}
|
|
);
|
|
}
|
|
|
|
void FRewindDebugger::OnRecordingStarted()
|
|
{
|
|
IterateExtensions([this](IRewindDebuggerExtension* Extension)
|
|
{
|
|
Extension->RecordingStarted(this);
|
|
}
|
|
);
|
|
|
|
UnrealInsightsModule->StartAnalysisForLastLiveSession(5.0);
|
|
}
|
|
|
|
void FRewindDebugger::OnRecordingStopped()
|
|
{
|
|
IterateExtensions([this](IRewindDebuggerExtension* Extension)
|
|
{
|
|
Extension->RecordingStopped(this);
|
|
}
|
|
);
|
|
}
|
|
|
|
bool FRewindDebugger::CanOpenTrace() const
|
|
{
|
|
return !bPIEStarted;
|
|
}
|
|
|
|
|
|
void FRewindDebugger::OpenTrace(const FString& FilePath)
|
|
{
|
|
ClearTrace();
|
|
|
|
bDisplayWorldIdValid = false;
|
|
|
|
IUnrealInsightsModule& TraceInsightsModule = FModuleManager::LoadModuleChecked<IUnrealInsightsModule>("TraceInsights");
|
|
TraceInsightsModule.StartAnalysisForTraceFile(*FilePath);
|
|
|
|
// todo: optionally open the map the trace file was recorded in
|
|
}
|
|
|
|
void FRewindDebugger::OpenTrace()
|
|
{
|
|
const FString FolderPath = "";
|
|
|
|
TArray<FString> OutOpenFilenames;
|
|
if (IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get())
|
|
{
|
|
FString ExtensionStr;
|
|
ExtensionStr += TEXT("Unreal Trace|*.utrace|");
|
|
|
|
DesktopPlatform->OpenFileDialog(
|
|
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
|
|
LOCTEXT("OpenDialogTitle", "Open Rewind Debugger Recording").ToString(),
|
|
FolderPath,
|
|
TEXT(""),
|
|
*ExtensionStr,
|
|
EFileDialogFlags::None,
|
|
OutOpenFilenames
|
|
);
|
|
}
|
|
|
|
if (OutOpenFilenames.Num() > 0)
|
|
{
|
|
if (OutOpenFilenames[0].EndsWith(TEXT("utrace")))
|
|
{
|
|
OpenTrace(OutOpenFilenames[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FRewindDebugger::AttachToSession()
|
|
{
|
|
ClearTrace();
|
|
const TSharedRef<SModalSessionBrowser> SessionBrowserModal = SNew(SModalSessionBrowser);
|
|
|
|
if (SessionBrowserModal->ShowModal() != EAppReturnType::Cancel)
|
|
{
|
|
bool bSuccess = false;
|
|
const SModalSessionBrowser::FTraceSessionInfo SessionInfo = SessionBrowserModal->GetSelectedTraceInfo();
|
|
if (SessionInfo.bIsValid)
|
|
{
|
|
const FString SessionAddress = SessionBrowserModal->GetSelectedTraceStoreAddress();
|
|
IUnrealInsightsModule& TraceInsightsModule = FModuleManager::LoadModuleChecked<IUnrealInsightsModule>("TraceInsights");
|
|
TraceInsightsModule.StartAnalysisForTrace(SessionInfo.TraceID);
|
|
bSuccess = TraceInsightsModule.GetAnalysisSession().IsValid();
|
|
}
|
|
|
|
if (!bSuccess)
|
|
{
|
|
FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("FailedToConnectToSessionMessage", "Failed to connect to session"));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FRewindDebugger::CanClearTrace() const
|
|
{
|
|
return GetAnalysisSession() != nullptr;
|
|
}
|
|
|
|
void FRewindDebugger::ClearTrace()
|
|
{
|
|
StopRecording();
|
|
RecordingDuration.Set(0);
|
|
|
|
CandidateIds.Empty();
|
|
CurrentTraceRange.SetLowerBoundValue(0);
|
|
CurrentTraceRange.SetUpperBoundValue(0);
|
|
RecordingDuration.Set(0.0);
|
|
SetCurrentScrubTime(0.0);
|
|
|
|
TrackSelectionChanged(nullptr);
|
|
|
|
// update extensions
|
|
IterateExtensions([this](IRewindDebuggerExtension* Extension)
|
|
{
|
|
Extension->Clear(this);
|
|
}
|
|
);
|
|
|
|
IUnrealInsightsModule& TraceInsightsModule = FModuleManager::LoadModuleChecked<IUnrealInsightsModule>("TraceInsights");
|
|
// only way I can find to clear the session is trying to load a name that doesn't exist.
|
|
TraceInsightsModule.StartAnalysisForTraceFile(TEXT("0"));
|
|
|
|
RefreshDebugTracks();
|
|
}
|
|
|
|
bool FRewindDebugger::CanSaveTrace() const
|
|
{
|
|
const TraceServices::IAnalysisSession* Session = GetAnalysisSession();
|
|
return Session != nullptr && Session->IsAnalysisComplete();
|
|
}
|
|
|
|
void FRewindDebugger::SaveTrace(FString FileName)
|
|
{
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
if (Session->IsAnalysisComplete())
|
|
{
|
|
const FString SourceFileName = Session->GetName();
|
|
|
|
FPlatformFileManager& FileManager = FPlatformFileManager::Get();
|
|
IPlatformFile& PlatformFile = FileManager.GetPlatformFile();
|
|
|
|
PlatformFile.CopyFile(*FileName, *SourceFileName);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::SaveTrace()
|
|
{
|
|
const FString FolderPath = "";
|
|
|
|
TArray<FString> OutFilenames;
|
|
if (IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get())
|
|
{
|
|
FString ExtensionStr;
|
|
ExtensionStr += TEXT("Rewind Debugger Recording |*.utrace|");
|
|
|
|
DesktopPlatform->SaveFileDialog(
|
|
FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr),
|
|
LOCTEXT("SaveDialogTitle", "Save Rewind Debugger Recording").ToString(),
|
|
FolderPath,
|
|
TEXT(""),
|
|
*ExtensionStr,
|
|
EFileDialogFlags::None,
|
|
OutFilenames
|
|
);
|
|
}
|
|
|
|
if (OutFilenames.Num() > 0)
|
|
{
|
|
if (OutFilenames[0].EndsWith(TEXT(".utrace")))
|
|
{
|
|
SaveTrace(OutFilenames[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FRewindDebugger::ShouldAutoRecordOnPIE() const
|
|
{
|
|
return URewindDebuggerSettings::Get().bShouldAutoRecordOnPIE;
|
|
}
|
|
|
|
void FRewindDebugger::SetShouldAutoRecordOnPIE(bool value)
|
|
{
|
|
URewindDebuggerSettings& RewindDebuggerSettings = URewindDebuggerSettings::Get();
|
|
RewindDebuggerSettings.Modify();
|
|
RewindDebuggerSettings.bShouldAutoRecordOnPIE = value;
|
|
RewindDebuggerSettings.SaveConfig();
|
|
}
|
|
|
|
bool FRewindDebugger::ShouldAutoEject() const
|
|
{
|
|
return URewindDebuggerSettings::Get().bShouldAutoEject;
|
|
}
|
|
|
|
void FRewindDebugger::SetShouldAutoEject(bool value)
|
|
{
|
|
URewindDebuggerSettings& RewindDebuggerSettings = URewindDebuggerSettings::Get();
|
|
RewindDebuggerSettings.Modify();
|
|
RewindDebuggerSettings.bShouldAutoEject = value;
|
|
RewindDebuggerSettings.SaveConfig();
|
|
}
|
|
|
|
void FRewindDebugger::StopRecording()
|
|
{
|
|
if (RewindDebugger::FRewindDebuggerRuntime* Runtime = RewindDebugger::FRewindDebuggerRuntime::Instance())
|
|
{
|
|
Runtime->StopRecording();
|
|
}
|
|
}
|
|
|
|
bool FRewindDebugger::CanPause() const
|
|
{
|
|
return ControlState != EControlState::Pause;
|
|
}
|
|
|
|
void FRewindDebugger::Pause()
|
|
{
|
|
if (CanPause())
|
|
{
|
|
if (bPIESimulating)
|
|
{
|
|
// pause PIE
|
|
}
|
|
|
|
ControlState = EControlState::Pause;
|
|
}
|
|
}
|
|
|
|
bool FRewindDebugger::IsPlaying() const
|
|
{
|
|
return ControlState == EControlState::Play && !bPIESimulating;
|
|
}
|
|
|
|
bool FRewindDebugger::CanPlay() const
|
|
{
|
|
return ControlState != EControlState::Play && !bPIESimulating && RecordingDuration.Get() > 0;
|
|
}
|
|
|
|
void FRewindDebugger::Play()
|
|
{
|
|
if (CanPlay())
|
|
{
|
|
if (CurrentScrubTime >= RecordingDuration.Get())
|
|
{
|
|
SetCurrentScrubTime(0);
|
|
}
|
|
|
|
ControlState = EControlState::Play;
|
|
}
|
|
}
|
|
|
|
bool FRewindDebugger::CanPlayReverse() const
|
|
{
|
|
return ControlState != EControlState::PlayReverse && !bPIESimulating && RecordingDuration.Get() > 0;
|
|
}
|
|
|
|
void FRewindDebugger::PlayReverse()
|
|
{
|
|
if (CanPlayReverse())
|
|
{
|
|
if (CurrentScrubTime <= 0)
|
|
{
|
|
SetCurrentScrubTime(RecordingDuration.Get());
|
|
}
|
|
|
|
ControlState = EControlState::PlayReverse;
|
|
}
|
|
}
|
|
|
|
bool FRewindDebugger::CanScrub() const
|
|
{
|
|
return !bPIESimulating && RecordingDuration.Get() > 0;
|
|
}
|
|
|
|
void FRewindDebugger::ScrubToStart()
|
|
{
|
|
if (CanScrub())
|
|
{
|
|
Pause();
|
|
SetCurrentScrubTime(0);
|
|
TrackCursorDelegate.ExecuteIfBound(false);
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::ScrubToEnd()
|
|
{
|
|
if (CanScrub())
|
|
{
|
|
Pause();
|
|
SetCurrentScrubTime(RecordingDuration.Get());
|
|
TrackCursorDelegate.ExecuteIfBound(false);
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::Step(const int32 InNumberOfFrames)
|
|
{
|
|
if (CanScrub())
|
|
{
|
|
Pause();
|
|
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
if (const IGameplayProvider* GameplayProvider = Session->ReadProvider<IGameplayProvider>("GameplayProvider"))
|
|
{
|
|
if (const IGameplayProvider::RecordingInfoTimeline* Recording = GameplayProvider->GetRecordingInfo(RecordingIndex))
|
|
{
|
|
const uint64 EventCount = Recording->GetEventCount();
|
|
|
|
if (EventCount > 0)
|
|
{
|
|
ScrubTimeInformation.FrameIndex = FMath::Clamp<int64>(ScrubTimeInformation.FrameIndex + InNumberOfFrames, 0, (int64)EventCount - 1);
|
|
const FRecordingInfoMessage& Event = Recording->GetEvent(ScrubTimeInformation.FrameIndex);
|
|
|
|
SetCurrentScrubTime(Event.ElapsedTime);
|
|
|
|
TrackCursorDelegate.ExecuteIfBound(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::StepForward()
|
|
{
|
|
Step(1);
|
|
}
|
|
|
|
void FRewindDebugger::StepBackward()
|
|
{
|
|
Step(-1);
|
|
}
|
|
|
|
|
|
void FRewindDebugger::ScrubToTime(double ScrubTime, bool bIsScrubbing)
|
|
{
|
|
if (CanScrub())
|
|
{
|
|
Pause();
|
|
SetCurrentScrubTime(ScrubTime);
|
|
}
|
|
}
|
|
|
|
UWorld* FRewindDebugger::GetWorldToVisualize() const
|
|
{
|
|
// we probably want to replace this with a world selector widget, if we are going to support tracing from anything other thn the PIE world
|
|
|
|
UWorld* World = nullptr;
|
|
|
|
UEditorEngine* EditorEngine = Cast<UEditorEngine>(GEngine);
|
|
if (GIsEditor && EditorEngine != nullptr && World == nullptr)
|
|
{
|
|
// let's use PlayWorld during PIE/Simulate and regular world from editor otherwise, to draw debug information
|
|
World = EditorEngine->PlayWorld != nullptr ? ToRawPtr(EditorEngine->PlayWorld) : EditorEngine->GetEditorWorldContext().World();
|
|
}
|
|
|
|
return World;
|
|
}
|
|
|
|
bool FRewindDebugger::IsRecording() const
|
|
{
|
|
if (const RewindDebugger::FRewindDebuggerRuntime* Runtime = RewindDebugger::FRewindDebuggerRuntime::Instance())
|
|
{
|
|
return Runtime->IsRecording();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FRewindDebugger::IsTraceFileLoaded() const
|
|
{
|
|
return GetAnalysisSession() != nullptr && !bPIEStarted;
|
|
}
|
|
|
|
void FRewindDebugger::SetCurrentViewRange(const TRange<double>& Range)
|
|
{
|
|
CurrentViewRange = Range;
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
GetScrubTimeInformation(CurrentViewRange.GetLowerBoundValue(), LowerBoundViewTimeInformation, RecordingIndex, Session);
|
|
GetScrubTimeInformation(CurrentViewRange.GetUpperBoundValue(), UpperBoundViewTimeInformation, RecordingIndex, Session);
|
|
|
|
CurrentTraceRange.SetLowerBoundValue(LowerBoundViewTimeInformation.ProfileTime);
|
|
CurrentTraceRange.SetUpperBoundValue(UpperBoundViewTimeInformation.ProfileTime);
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::SetCurrentScrubTime(double Time)
|
|
{
|
|
CurrentScrubTime = Time;
|
|
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
GetScrubTimeInformation(CurrentScrubTime, ScrubTimeInformation, RecordingIndex, Session);
|
|
|
|
TraceTime.Set(ScrubTimeInformation.ProfileTime);
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::GetScrubTimeInformation(double InDebugTime, FScrubTimeInformation& InOutTimeInformation, uint16 InRecordingIndex, const TraceServices::IAnalysisSession* AnalysisSession)
|
|
{
|
|
const IGameplayProvider* GameplayProvider = AnalysisSession->ReadProvider<IGameplayProvider>("GameplayProvider");
|
|
const IAnimationProvider* AnimationProvider = AnalysisSession->ReadProvider<IAnimationProvider>("AnimationProvider");
|
|
|
|
if (GameplayProvider && AnimationProvider)
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*AnalysisSession);
|
|
|
|
if (const IGameplayProvider::RecordingInfoTimeline* Recording = GameplayProvider->GetRecordingInfo(InRecordingIndex))
|
|
{
|
|
const uint64 EventCount = Recording->GetEventCount();
|
|
|
|
if (EventCount > 0)
|
|
{
|
|
int ScrubFrameIndex = InOutTimeInformation.FrameIndex;
|
|
const FRecordingInfoMessage& FirstEvent = Recording->GetEvent(0);
|
|
const FRecordingInfoMessage& LastEvent = Recording->GetEvent(EventCount - 1);
|
|
|
|
// Check if we are outside the recorded range, and apply the first or last frame
|
|
if (InDebugTime <= FirstEvent.ElapsedTime)
|
|
{
|
|
ScrubFrameIndex = FMath::Min<uint64>(1, EventCount - 1);
|
|
}
|
|
else if (InDebugTime >= LastEvent.ElapsedTime)
|
|
{
|
|
ScrubFrameIndex = EventCount - 1;
|
|
}
|
|
// Find the two keys surrounding the InDebugTime, and pick the nearest to update InOutTimeInformation
|
|
else
|
|
{
|
|
const FRecordingInfoMessage& ScrubEvent = Recording->GetEvent(ScrubFrameIndex);
|
|
constexpr float MaxTimeDifferenceInSeconds = 15.0f / 60.0f;
|
|
|
|
// Use linear search on smaller time differences
|
|
if (FMath::Abs(InDebugTime - ScrubEvent.ElapsedTime) <= MaxTimeDifferenceInSeconds)
|
|
{
|
|
if (Recording->GetEvent(ScrubFrameIndex).ElapsedTime > InDebugTime)
|
|
{
|
|
for (uint64 EventIndex = ScrubFrameIndex; EventIndex > 0; EventIndex--)
|
|
{
|
|
const FRecordingInfoMessage& Event = Recording->GetEvent(EventIndex);
|
|
const FRecordingInfoMessage& NextEvent = Recording->GetEvent(EventIndex - 1);
|
|
if (Event.ElapsedTime >= InDebugTime && NextEvent.ElapsedTime <= InDebugTime)
|
|
{
|
|
if (Event.ElapsedTime - InDebugTime < InDebugTime - NextEvent.ElapsedTime)
|
|
{
|
|
ScrubFrameIndex = EventIndex;
|
|
}
|
|
else
|
|
{
|
|
ScrubFrameIndex = EventIndex - 1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (uint64 EventIndex = ScrubFrameIndex; EventIndex < EventCount - 1; EventIndex++)
|
|
{
|
|
const FRecordingInfoMessage& Event = Recording->GetEvent(EventIndex);
|
|
const FRecordingInfoMessage& NextEvent = Recording->GetEvent(EventIndex + 1);
|
|
if (Event.ElapsedTime <= InDebugTime && NextEvent.ElapsedTime >= InDebugTime)
|
|
{
|
|
if (InDebugTime - Event.ElapsedTime < NextEvent.ElapsedTime - InDebugTime)
|
|
{
|
|
ScrubFrameIndex = EventIndex;
|
|
}
|
|
else
|
|
{
|
|
ScrubFrameIndex = EventIndex + 1;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Binary search for surrounding keys on big time differences
|
|
else
|
|
{
|
|
uint64 StartEventIndex = 0;
|
|
uint64 EndEventIndex = EventCount - 1;
|
|
|
|
while (EndEventIndex - StartEventIndex > 1)
|
|
{
|
|
const uint64 MiddleEventIndex = ((StartEventIndex + EndEventIndex) / 2);
|
|
const FRecordingInfoMessage& MiddleEvent = Recording->GetEvent(MiddleEventIndex);
|
|
if (InDebugTime < MiddleEvent.ElapsedTime)
|
|
{
|
|
EndEventIndex = MiddleEventIndex;
|
|
}
|
|
else
|
|
{
|
|
StartEventIndex = MiddleEventIndex;
|
|
}
|
|
}
|
|
|
|
// Ensure there is not frames between start and end index
|
|
check(EndEventIndex == StartEventIndex + 1)
|
|
|
|
const FRecordingInfoMessage& Event = Recording->GetEvent(StartEventIndex);
|
|
const FRecordingInfoMessage& NextEvent = Recording->GetEvent(EndEventIndex);
|
|
|
|
// Ensure debug time is between both frames time range
|
|
check(Event.ElapsedTime <= InDebugTime && NextEvent.ElapsedTime >= InDebugTime)
|
|
|
|
// Choose frame that is nearest to the debug time
|
|
if (InDebugTime - Event.ElapsedTime < NextEvent.ElapsedTime - InDebugTime)
|
|
{
|
|
ScrubFrameIndex = StartEventIndex;
|
|
}
|
|
else
|
|
{
|
|
ScrubFrameIndex = EndEventIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
const FRecordingInfoMessage& Event = Recording->GetEvent(ScrubFrameIndex);
|
|
InOutTimeInformation.FrameIndex = ScrubFrameIndex;
|
|
InOutTimeInformation.ProfileTime = Event.ProfileTime;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const TraceServices::IAnalysisSession* FRewindDebugger::GetAnalysisSession() const
|
|
{
|
|
if (UnrealInsightsModule == nullptr)
|
|
{
|
|
UnrealInsightsModule = &FModuleManager::LoadModuleChecked<IUnrealInsightsModule>("TraceInsights");
|
|
}
|
|
|
|
return UnrealInsightsModule ? UnrealInsightsModule->GetAnalysisSession().Get() : nullptr;
|
|
}
|
|
|
|
const FObjectInfo* FRewindDebugger::FindOwningActorInfo(const IGameplayProvider* GameplayProvider, uint64 InObjectId) const
|
|
{
|
|
const FClassInfo* ActorClassInfo = GameplayProvider->FindClassInfo(*AActor::StaticClass()->GetPathName());
|
|
|
|
while (true)
|
|
{
|
|
const FObjectInfo& ObjectInfo = GameplayProvider->GetObjectInfo(InObjectId);
|
|
if (GameplayProvider->IsSubClassOf(ObjectInfo.ClassId, ActorClassInfo->Id))
|
|
{
|
|
return &ObjectInfo;
|
|
}
|
|
|
|
if (!ObjectInfo.GetOuterId().IsSet())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
InObjectId = ObjectInfo.GetOuterUObjectId();
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::Tick(float DeltaTime)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FRewindDebugger::Tick);
|
|
|
|
if (bQueueStartRecording)
|
|
{
|
|
StartRecording();
|
|
bQueueStartRecording = false;
|
|
}
|
|
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
const IAnimationProvider* AnimationProvider = Session->ReadProvider<IAnimationProvider>("AnimationProvider");
|
|
const IGameplayProvider* GameplayProvider = Session->ReadProvider<IGameplayProvider>("GameplayProvider");
|
|
|
|
if (AnimationProvider && GameplayProvider)
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
// set a default display world when loading a trace (first client/standalone world)
|
|
if (IsTraceFileLoaded() && !bDisplayWorldIdValid)
|
|
{
|
|
GameplayProvider->EnumerateWorlds([this](const FWorldInfo& WorldInfo)
|
|
{
|
|
if (WorldInfo.Type == FWorldInfo::EType::PIE)
|
|
{
|
|
if (WorldInfo.NetMode == FWorldInfo::ENetMode::Client && WorldInfo.PIEInstanceId == 1)
|
|
{
|
|
DisplayWorldId = WorldInfo.Id;
|
|
bDisplayWorldIdValid = true;
|
|
}
|
|
if (WorldInfo.NetMode == FWorldInfo::ENetMode::Standalone && WorldInfo.PIEInstanceId == 0)
|
|
{
|
|
DisplayWorldId = WorldInfo.Id;
|
|
bDisplayWorldIdValid = true;
|
|
}
|
|
}
|
|
else if (WorldInfo.Type == FWorldInfo::EType::Game)
|
|
{
|
|
DisplayWorldId = WorldInfo.Id;
|
|
bDisplayWorldIdValid = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
const double RecordingDurationValue = GameplayProvider->GetRecordingDuration();
|
|
if (IsTraceFileLoaded() && RecordingDurationValue > RecordingDuration.Get())
|
|
{
|
|
// while trace file is loading up, force the trace range to update.
|
|
SetCurrentViewRange(GetCurrentViewRange());
|
|
}
|
|
RecordingDuration.Set(RecordingDurationValue);
|
|
|
|
RefreshDebugTracks();
|
|
|
|
if (bPIESimulating)
|
|
{
|
|
if (IsRecording())
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FRewindDebugger::Tick_UpdateSimulating);
|
|
SetCurrentScrubTime(RecordingDurationValue);
|
|
TrackCursorDelegate.ExecuteIfBound(false);
|
|
}
|
|
bTargetActorPositionValid = false;
|
|
}
|
|
else
|
|
{
|
|
if (RecordingDuration.Get() > 0 && CurrentScrubTime <= RecordingDuration.Get())
|
|
{
|
|
if (ControlState == EControlState::Play || ControlState == EControlState::PlayReverse)
|
|
{
|
|
const float PlaybackRate = URewindDebuggerSettings::Get().PlaybackRate;
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FRewindDebugger::Tick_UpdatePlayback);
|
|
const float Rate = PlaybackRate * (ControlState == EControlState::Play ? 1 : -1);
|
|
SetCurrentScrubTime(FMath::Clamp(CurrentScrubTime + Rate * DeltaTime, 0.0f, RecordingDuration.Get()));
|
|
TrackCursorDelegate.ExecuteIfBound(Rate < 0);
|
|
|
|
if (CurrentScrubTime == 0 || CurrentScrubTime == RecordingDuration.Get())
|
|
{
|
|
// pause at end.
|
|
ControlState = EControlState::Pause;
|
|
}
|
|
}
|
|
|
|
SetCurrentScrubTime(CurrentScrubTime);// update trace time
|
|
|
|
const double CurrentTraceTime = TraceTime.Get();
|
|
if (CurrentTraceTime != PreviousTraceTime)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(FRewindDebugger::Tick_UpdateActorPosition);
|
|
PreviousTraceTime = CurrentTraceTime;
|
|
|
|
const TraceServices::IFrameProvider& FrameProvider = TraceServices::ReadFrameProvider(*Session);
|
|
TraceServices::FFrame Frame;
|
|
if (FrameProvider.GetFrameFromTime(ETraceFrameType::TraceFrameType_Game, CurrentTraceTime, Frame))
|
|
{
|
|
bool bNewActor = false;
|
|
if (!CandidateIds.Contains(RewindDebugger::FObjectId{TargetActorIdForMesh}))
|
|
{
|
|
AnimationProvider->EnumerateSkeletalMeshPoseTimelines([this, &bNewActor, GameplayProvider](uint64 ObjectId, const IAnimationProvider::SkeletalMeshPoseTimeline& TimelineData)
|
|
{
|
|
// until we have actor transforms traced out, the first (from a non-server) skeletal mesh component transform on the target actor be used as the actor position
|
|
|
|
if (const FWorldInfo* WorldInfo = GameplayProvider->FindWorldInfoFromObject(ObjectId))
|
|
{
|
|
if (WorldInfo->NetMode != FWorldInfo::ENetMode::DedicatedServer)
|
|
{
|
|
if (const FObjectInfo* ActorInfo = FindOwningActorInfo(GameplayProvider, ObjectId))
|
|
{
|
|
if (CandidateIds.Contains(ActorInfo->GetId()))
|
|
{
|
|
bNewActor = true;
|
|
TargetActorIdForMesh = ActorInfo->GetUObjectId();
|
|
TargetActorMeshId = ObjectId;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
AnimationProvider->ReadSkeletalMeshPoseTimeline(TargetActorMeshId, [this, &Frame, bNewActor](const IAnimationProvider::SkeletalMeshPoseTimeline& TimelineData, bool bHasCurves)
|
|
{
|
|
const FSkeletalMeshPoseMessage* PoseMessage = nullptr;
|
|
|
|
// Get last pose in frame
|
|
TimelineData.EnumerateEvents(Frame.StartTime, Frame.EndTime,
|
|
[&PoseMessage](double InStartTime, double InEndTime, uint32 InDepth, const FSkeletalMeshPoseMessage& InPoseMessage)
|
|
{
|
|
PoseMessage = &InPoseMessage;
|
|
return TraceServices::EEventEnumerate::Continue;
|
|
});
|
|
|
|
// Update position based on pose
|
|
if (PoseMessage)
|
|
{
|
|
// mark the target position as invalid for a frame when the actor changes, so it will be treated as a teleport by the camera system
|
|
bTargetActorPositionValid = !bNewActor;
|
|
TargetActorPosition = PoseMessage->ComponentToWorld.GetTranslation();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// update extensions
|
|
IterateExtensions([DeltaTime, this](IRewindDebuggerExtension* Extension)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(*Extension->GetName());
|
|
Extension->Update(DeltaTime, this);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::OpenDetailsPanel()
|
|
{
|
|
bIsDetailsPanelOpen = true;
|
|
TrackSelectionChanged(SelectedTrack);
|
|
}
|
|
|
|
void FRewindDebugger::TrackSelectionChanged(const TSharedPtr<RewindDebugger::FRewindDebuggerTrack> InSelectedTrack)
|
|
{
|
|
SelectedTrack = InSelectedTrack;
|
|
|
|
if (bIsDetailsPanelOpen)
|
|
{
|
|
const FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked<FLevelEditorModule>("LevelEditor");
|
|
const TSharedPtr<FTabManager> LevelEditorTabManager = LevelEditorModule.GetLevelEditorTabManager();
|
|
|
|
// if we now have no selection, don't force the tab into focus - this happens when tracks disappear and can cause PIE to lose focus while playing
|
|
const bool bInvokeAsInactive = !SelectedTrack.IsValid();
|
|
const TSharedPtr<SDockTab> DetailsTab = LevelEditorTabManager->TryInvokeTab(FRewindDebuggerModule::DetailsTabName, bInvokeAsInactive);
|
|
|
|
if (DetailsTab.IsValid())
|
|
{
|
|
UpdateDetailsPanel(DetailsTab.ToSharedRef());
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::UpdateDetailsPanel(TSharedRef<SDockTab> DetailsTab)
|
|
{
|
|
if (bIsDetailsPanelOpen)
|
|
{
|
|
TSharedPtr<SWidget> DetailsView;
|
|
|
|
if (SelectedTrack)
|
|
{
|
|
DetailsView = SelectedTrack->GetDetailsView();
|
|
}
|
|
|
|
if (DetailsView)
|
|
{
|
|
DetailsTab->SetContent(DetailsView.ToSharedRef());
|
|
}
|
|
else
|
|
{
|
|
static TSharedPtr<SWidget> EmptyDetails;
|
|
if (EmptyDetails == nullptr)
|
|
{
|
|
EmptyDetails = SNew(SSpacer);
|
|
}
|
|
DetailsTab->SetContent(EmptyDetails.ToSharedRef());
|
|
}
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::RegisterTrackContextMenu()
|
|
{
|
|
UToolMenu* Menu = UToolMenus::Get()->FindMenu(FRewindDebuggerModule::TrackContextMenuName);
|
|
|
|
FToolMenuSection& Section = Menu->FindOrAddSection("SelectedTrack");
|
|
|
|
Section.AddDynamicEntry(NAME_None, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection)
|
|
{
|
|
const URewindDebuggerTrackContextMenuContext* Context = InSection.FindContext<URewindDebuggerTrackContextMenuContext>();
|
|
if (Context && Context->SelectedTrack.IsValid())
|
|
{
|
|
Context->SelectedTrack->BuildContextMenu(InSection);
|
|
}
|
|
}));
|
|
}
|
|
|
|
void FRewindDebugger::MakeOtherWorldsMenu(UToolMenu* Menu)
|
|
{
|
|
const FRewindDebugger* RewindDebugger = FRewindDebugger::Instance();
|
|
|
|
FToolMenuSection& Section = Menu->AddSection("Other Worlds", LOCTEXT("Other Worlds", "Other Worlds"));
|
|
|
|
if (const TraceServices::IAnalysisSession* Session = RewindDebugger->GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
const IGameplayProvider* GameplayProvider = Session->ReadProvider<IGameplayProvider>("GameplayProvider");
|
|
|
|
GameplayProvider->EnumerateWorlds([GameplayProvider, &Section](const FWorldInfo& WorldInfo)
|
|
{
|
|
const FObjectInfo* ObjectInfo = GameplayProvider->FindObjectInfo(WorldInfo.Id);
|
|
FString Name = ObjectInfo->Name;
|
|
|
|
if (WorldInfo.NetMode == FWorldInfo::ENetMode::DedicatedServer)
|
|
{
|
|
return;
|
|
}
|
|
else if (WorldInfo.Type == FWorldInfo::EType::Game || WorldInfo.Type == FWorldInfo::EType::PIE)
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (WorldInfo.Type == FWorldInfo::EType::Editor)
|
|
{
|
|
Name = Name + " (Editor)";
|
|
}
|
|
else if (WorldInfo.Type == FWorldInfo::EType::Inactive)
|
|
{
|
|
Name = Name + " (Editor)";
|
|
}
|
|
else if (WorldInfo.Type == FWorldInfo::EType::EditorPreview)
|
|
{
|
|
Name = Name + " (Editor Preview)";
|
|
}
|
|
else if (WorldInfo.Type == FWorldInfo::EType::GamePreview)
|
|
{
|
|
Name = Name + " (Game Preview)";
|
|
}
|
|
else if (WorldInfo.Type == FWorldInfo::EType::GameRPC)
|
|
{
|
|
Name = Name + " (Game RPC)";
|
|
}
|
|
}
|
|
|
|
Section.AddMenuEntry(FName(ObjectInfo->Name, WorldInfo.Id),
|
|
FText::FromString(Name),
|
|
FText(),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateLambda([World = WorldInfo.Id]()
|
|
{
|
|
FRewindDebugger::Instance()->SetDisplayWorld(World);
|
|
}),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([World = WorldInfo.Id]()
|
|
{
|
|
return FRewindDebugger::Instance()->DisplayWorldId == World;
|
|
})),
|
|
EUserInterfaceActionType::Check
|
|
);
|
|
|
|
});
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::SetDisplayWorld(uint64 WorldId)
|
|
{
|
|
DisplayWorldId = WorldId;
|
|
|
|
IterateExtensions([this](IRewindDebuggerExtension* Extension)
|
|
{
|
|
Extension->Clear(this);
|
|
Extension->Update(0.0, this);
|
|
});
|
|
}
|
|
void FRewindDebugger::MakeWorldsMenu(UToolMenu* Menu)
|
|
{
|
|
const FRewindDebugger* RewindDebugger = FRewindDebugger::Instance();
|
|
|
|
FToolMenuSection& ServerWorldsSection = Menu->AddSection("Server Worlds", LOCTEXT("Server", "Server"));
|
|
FToolMenuSection& GameWorldsSection = Menu->AddSection("Game Worlds", LOCTEXT("Game Worlds", "Game Worlds"));
|
|
FToolMenuSection& OtherWorldsSection = Menu->AddSection("Other Worlds", LOCTEXT("Other Worlds", "Other Worlds"));
|
|
|
|
OtherWorldsSection.AddSubMenu("Other Worlds",
|
|
LOCTEXT("Other Worlds", "Other Worlds"),
|
|
LOCTEXT("Other Worlds Tooltip", "Additional worlds such as Editor Preview worlds"),
|
|
FNewToolMenuChoice(
|
|
FNewToolMenuDelegate::CreateStatic(FRewindDebugger::MakeOtherWorldsMenu)
|
|
));
|
|
|
|
if (const TraceServices::IAnalysisSession* Session = RewindDebugger->GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
const IGameplayProvider* GameplayProvider = Session->ReadProvider<IGameplayProvider>("GameplayProvider");
|
|
|
|
GameplayProvider->EnumerateWorlds([GameplayProvider, &GameWorldsSection, &OtherWorldsSection, &ServerWorldsSection](const FWorldInfo& WorldInfo)
|
|
{
|
|
const FObjectInfo* ObjectInfo = GameplayProvider->FindObjectInfo(WorldInfo.Id);
|
|
FString Name = ObjectInfo->Name;
|
|
|
|
FToolMenuSection* Section = &OtherWorldsSection;
|
|
|
|
if (WorldInfo.NetMode == FWorldInfo::ENetMode::DedicatedServer)
|
|
{
|
|
Section = &ServerWorldsSection;
|
|
Name = Name + " (Server)";
|
|
}
|
|
else if (WorldInfo.Type == FWorldInfo::EType::Game || WorldInfo.Type == FWorldInfo::EType::PIE)
|
|
{
|
|
Section = &GameWorldsSection;
|
|
if (WorldInfo.NetMode == FWorldInfo::ENetMode::Client && WorldInfo.PIEInstanceId >= 0)
|
|
{
|
|
Name = Name + " (Client " + FString::FromInt(WorldInfo.PIEInstanceId) + ")";
|
|
}
|
|
if (WorldInfo.NetMode == FWorldInfo::ENetMode::Standalone && WorldInfo.PIEInstanceId >= 0)
|
|
{
|
|
Name = Name + " (Standalone " + FString::FromInt(WorldInfo.PIEInstanceId) + ")";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
Section->AddMenuEntry(FName(ObjectInfo->Name, WorldInfo.Id),
|
|
FText::FromString(Name),
|
|
FText(),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateLambda([World = WorldInfo.Id]()
|
|
{
|
|
FRewindDebugger::Instance()->SetDisplayWorld(World);
|
|
}),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([World = WorldInfo.Id]()
|
|
{
|
|
return FRewindDebugger::Instance()->DisplayWorldId == World;
|
|
})),
|
|
EUserInterfaceActionType::Check
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
void FRewindDebugger::RegisterToolBar()
|
|
{
|
|
UToolMenu* Menu = UToolMenus::Get()->RegisterMenu("RewindDebugger.ToolBar", NAME_None, EMultiBoxType::ToolBar);
|
|
|
|
FToolMenuSection& Section = Menu->FindOrAddSection("VCRControls");
|
|
const FRewindDebuggerCommands& Commands = FRewindDebuggerCommands::Get();
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.FirstFrame,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.FirstFrame.small")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.PreviousFrame,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.PreviousFrame.small")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.ReversePlay,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.ReversePlay.small")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.Pause,
|
|
FText(),
|
|
FText::Format(LOCTEXT("PauseButtonTooltip", "{0} ({1})"), Commands.Pause->GetDescription(), Commands.PauseOrPlay->GetInputText()),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.Pause.small")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.Play,
|
|
FText(),
|
|
FText::Format(LOCTEXT("PlayButtonTooltip", "{0} ({1})"), Commands.Play->GetDescription(), Commands.PauseOrPlay->GetInputText()),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.Play.small")));
|
|
|
|
Section.AddEntry(
|
|
FToolMenuEntry::InitComboButton(
|
|
"PlaybackRate",
|
|
FToolUIActionChoice(),
|
|
FNewToolMenuChoice(
|
|
FNewToolMenuDelegate::CreateLambda([](UToolMenu* InNewToolMenu)
|
|
{
|
|
FToolMenuSection& Section = InNewToolMenu->AddSection("PlaybackSpeed", LOCTEXT("Playback Speed", "Playback Speed"));
|
|
|
|
Section.AddEntry(
|
|
FToolMenuEntry::InitMenuEntry(
|
|
"001", LOCTEXT("0.1", "0.1"), LOCTEXT("Set playback speed to 0.1", "Set playback speed to 0.1"), FSlateIcon(),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda([]()
|
|
{
|
|
URewindDebuggerSettings::Get().PlaybackRate = 0.1;
|
|
}),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([]
|
|
{
|
|
return FMath::IsNearlyEqual(URewindDebuggerSettings::Get().PlaybackRate, 0.1);
|
|
})
|
|
)
|
|
, EUserInterfaceActionType::RadioButton
|
|
)
|
|
);
|
|
Section.AddEntry(
|
|
FToolMenuEntry::InitMenuEntry(
|
|
"025", LOCTEXT("0.25", "0.25"), LOCTEXT("Set playback speed to 0.25", "Set playback speed to 0.25"), FSlateIcon(),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda([]()
|
|
{
|
|
URewindDebuggerSettings::Get().PlaybackRate = 0.25;
|
|
}),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([]
|
|
{
|
|
return FMath::IsNearlyEqual(URewindDebuggerSettings::Get().PlaybackRate, 0.25);
|
|
})
|
|
)
|
|
, EUserInterfaceActionType::RadioButton
|
|
)
|
|
);
|
|
Section.AddEntry(
|
|
FToolMenuEntry::InitMenuEntry(
|
|
"05", LOCTEXT("0.5", "0.5"), LOCTEXT("Set playback speed to 0.5", "Set playback speed to 0.5"), FSlateIcon(),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda([]()
|
|
{
|
|
URewindDebuggerSettings::Get().PlaybackRate = 0.5;
|
|
}),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([]
|
|
{
|
|
return FMath::IsNearlyEqual(URewindDebuggerSettings::Get().PlaybackRate, 0.5);
|
|
})
|
|
)
|
|
, EUserInterfaceActionType::RadioButton
|
|
)
|
|
);
|
|
|
|
Section.AddEntry(
|
|
FToolMenuEntry::InitMenuEntry(
|
|
"1", LOCTEXT("1", "1"), LOCTEXT("Set playback speed to 1", "Set playback speed to 1"), FSlateIcon(),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda([]()
|
|
{
|
|
URewindDebuggerSettings::Get().PlaybackRate = 1;
|
|
}),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([]
|
|
{
|
|
return FMath::IsNearlyEqual(URewindDebuggerSettings::Get().PlaybackRate, 1);
|
|
})
|
|
)
|
|
, EUserInterfaceActionType::RadioButton
|
|
)
|
|
);
|
|
|
|
Section.AddEntry(
|
|
FToolMenuEntry::InitMenuEntry(
|
|
"2", LOCTEXT("2", "2"), LOCTEXT("Set playback speed to 2", "Set playback speed to 2"), FSlateIcon(),
|
|
FUIAction(
|
|
FExecuteAction::CreateLambda([]()
|
|
{
|
|
URewindDebuggerSettings::Get().PlaybackRate = 2;
|
|
}),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateLambda([]
|
|
{
|
|
return FMath::IsNearlyEqual(URewindDebuggerSettings::Get().PlaybackRate, 2);
|
|
})
|
|
)
|
|
, EUserInterfaceActionType::RadioButton
|
|
)
|
|
);
|
|
|
|
Section.AddEntry(
|
|
FToolMenuEntry::InitWidget(
|
|
"EditInSequencerMenu",
|
|
SNew(SNumericEntryBox<float>)
|
|
.Value_Lambda([]()
|
|
{
|
|
return URewindDebuggerSettings::Get().PlaybackRate;
|
|
})
|
|
.OnValueChanged_Lambda([](float Value)
|
|
{
|
|
URewindDebuggerSettings::Get().PlaybackRate = Value;
|
|
}),
|
|
FText::GetEmpty(),
|
|
true, false, true
|
|
)
|
|
);
|
|
})
|
|
),
|
|
FText(),
|
|
LOCTEXT("PlaybackRate_Tooltip", "Playback Options"),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Sequencer.PlaybackOptions")
|
|
)
|
|
);
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.NextFrame,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.NextFrame.small")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.LastFrame,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.LastFrame.small")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.StartRecording,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.StartRecording.small")));
|
|
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.StopRecording,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.StopRecording.small")));
|
|
|
|
Section.AddSeparator(NAME_None);
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.AttachToSession,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.ConnectToSession")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.OpenTrace,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.FolderOpen")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.SaveTrace,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Save")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.ClearTrace,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete")));
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.AutoEject,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.AutoEject")));
|
|
|
|
Section.AddSeparator(NAME_None);
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitToolBarButton(
|
|
Commands.AutoRecord,
|
|
FText(),
|
|
TAttribute<FText>(),
|
|
FSlateIcon("RewindDebuggerStyle", "RewindDebugger.AutoRecord")));
|
|
|
|
Section.AddSeparator("NAME_None");
|
|
|
|
Section.AddEntry(FToolMenuEntry::InitComboButton(
|
|
"Display World",
|
|
FUIAction(
|
|
FExecuteAction(),
|
|
FCanExecuteAction::CreateLambda([]() { return FRewindDebugger::Instance()->IsTraceFileLoaded(); })
|
|
),
|
|
FNewToolMenuDelegate::CreateStatic(&FRewindDebugger::MakeWorldsMenu),
|
|
LOCTEXT("Display World", "Display World"),
|
|
LOCTEXT("Display World Tooltip", "When loading trace files, only the objects (Such as Skeletal Meshes) from the world selected here will be spawned for preview")
|
|
));
|
|
|
|
Menu->SetStyleSet(&FAppStyle::Get());
|
|
Menu->StyleName = "PaletteToolBar";
|
|
}
|
|
|
|
|
|
void FRewindDebugger::TrackDoubleClicked(TSharedPtr<RewindDebugger::FRewindDebuggerTrack> InSelectedTrack)
|
|
{
|
|
if (!InSelectedTrack.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
SelectedTrack = InSelectedTrack;
|
|
SelectedTrack->HandleDoubleClick();
|
|
}
|
|
|
|
TSharedPtr<SWidget> FRewindDebugger::BuildTrackContextMenu() const
|
|
{
|
|
URewindDebuggerTrackContextMenuContext* MenuContext = NewObject<URewindDebuggerTrackContextMenuContext>();
|
|
MenuContext->SelectedObject = GetSelectedObject();
|
|
MenuContext->SelectedTrack = SelectedTrack;
|
|
|
|
if (SelectedTrack.IsValid())
|
|
{
|
|
// build a list of class hierarchy names to make it easier for extensions to enable menu entries by type
|
|
if (const TraceServices::IAnalysisSession* Session = GetAnalysisSession())
|
|
{
|
|
TraceServices::FAnalysisSessionReadScope SessionReadScope(*Session);
|
|
|
|
const IGameplayProvider* GameplayProvider = Session->ReadProvider<IGameplayProvider>("GameplayProvider");
|
|
const FObjectInfo& ObjectInfo = GameplayProvider->GetObjectInfo(SelectedTrack->GetAssociatedObjectId());
|
|
uint64 ClassId = ObjectInfo.ClassId;
|
|
while (ClassId != 0)
|
|
{
|
|
const FClassInfo& ClassInfo = GameplayProvider->GetClassInfo(ClassId);
|
|
MenuContext->TypeHierarchy.Add(ClassInfo.Name);
|
|
ClassId = ClassInfo.SuperId;
|
|
}
|
|
}
|
|
}
|
|
|
|
return UToolMenus::Get()->GenerateWidget(FRewindDebuggerModule::TrackContextMenuName, FToolMenuContext(MenuContext));
|
|
}
|
|
|
|
|
|
TSharedPtr<FDebugObjectInfo> FRewindDebugger::GetSelectedObject() const
|
|
{
|
|
if (SelectedTrack.IsValid())
|
|
{
|
|
if (!SelectedObject.IsValid())
|
|
{
|
|
SelectedObject = MakeShared<FDebugObjectInfo>();
|
|
}
|
|
|
|
SelectedObject->Id = SelectedTrack->GetAssociatedObjectId();
|
|
SelectedObject->ObjectName = SelectedTrack->GetDisplayName().ToString();
|
|
return SelectedObject;
|
|
}
|
|
|
|
return TSharedPtr<FDebugObjectInfo>();
|
|
}
|
|
|
|
TSharedPtr<RewindDebugger::FRewindDebuggerTrack> FRewindDebugger::GetSelectedTrack() const
|
|
{
|
|
return SelectedTrack;
|
|
}
|
|
|
|
// build a tree that's compatible with the public api from 5.0 for GetDebuggedObjects.
|
|
void FRewindDebugger::RefreshDebuggedObjects(TArray<TSharedPtr<RewindDebugger::FRewindDebuggerTrack>>& InTracks, TArray<TSharedPtr<FDebugObjectInfo>>& OutObjects)
|
|
{
|
|
OutObjects.SetNum(0, EAllowShrinking::No);
|
|
for (const TSharedPtr<RewindDebugger::FRewindDebuggerTrack>& Track : InTracks)
|
|
{
|
|
const int Index = OutObjects.Add(MakeShared<FDebugObjectInfo>(Track->GetAssociatedObjectId(), Track->GetDisplayName().ToString()));
|
|
TArray<TSharedPtr<RewindDebugger::FRewindDebuggerTrack>> TrackChildren;
|
|
Track->IterateSubTracks([&TrackChildren](const TSharedPtr<RewindDebugger::FRewindDebuggerTrack>& Child) { TrackChildren.Add(Child); });
|
|
RefreshDebuggedObjects(TrackChildren, OutObjects[Index]->Children);
|
|
}
|
|
}
|
|
|
|
TArray<TSharedPtr<FDebugObjectInfo>>& FRewindDebugger::GetDebuggedObjects()
|
|
{
|
|
RefreshDebuggedObjects(DebugTracks, DebuggedObjects);
|
|
return DebuggedObjects;
|
|
}
|
|
|
|
bool FRewindDebugger::IsObjectCurrentlyDebugged(uint64 ObjectId) const
|
|
{
|
|
for (const TSharedPtr<RewindDebugger::FRewindDebuggerTrack>& Track : DebugTracks)
|
|
{
|
|
if (Track->GetUObjectId() == ObjectId)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool Found = false;
|
|
Track->IterateSubTracks([ObjectId, &Found](const TSharedPtr<RewindDebugger::FRewindDebuggerTrack>& Child)
|
|
{
|
|
if (Child->GetUObjectId() == ObjectId)
|
|
{
|
|
Found = true;
|
|
// @Todo STDBG: want to stop iteration here
|
|
}
|
|
});
|
|
|
|
if (Found)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|