// 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 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(ModularFeatures.GetModularFeatureImplementation(IRewindDebuggerExtension::ModularFeatureName, ExtensionIndex)); IteratorFunction(Extension); } } static void TraceSubobjects(const UObject* OuterObject) { TArray 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("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("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& 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("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(FName(SelectionName), FText::FromString(SelectionName)); DebugTracks[1] = MakeShared(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* FoundTrack = DebugTracks.FindByPredicate( [CandidateIdentifier](const TSharedPtr& Track) { return Track->GetAssociatedObjectId() == CandidateIdentifier; }); if (!FoundTrack) { DebugTracks.Add(MakeShared(CandidateIdentifier, SelectedObjectName.Get(), true)); bChanged = true; } } // update all tracks for (const TSharedPtr& 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("TraceInsights"); TraceInsightsModule.StartAnalysisForTraceFile(*FilePath); // todo: optionally open the map the trace file was recorded in } void FRewindDebugger::OpenTrace() { const FString FolderPath = ""; TArray 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 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("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("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 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("GameplayProvider")) { if (const IGameplayProvider::RecordingInfoTimeline* Recording = GameplayProvider->GetRecordingInfo(RecordingIndex)) { const uint64 EventCount = Recording->GetEventCount(); if (EventCount > 0) { ScrubTimeInformation.FrameIndex = FMath::Clamp(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(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& 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("GameplayProvider"); const IAnimationProvider* AnimationProvider = AnalysisSession->ReadProvider("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(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("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("AnimationProvider"); const IGameplayProvider* GameplayProvider = Session->ReadProvider("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 InSelectedTrack) { SelectedTrack = InSelectedTrack; if (bIsDetailsPanelOpen) { const FLevelEditorModule& LevelEditorModule = FModuleManager::GetModuleChecked("LevelEditor"); const TSharedPtr 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 DetailsTab = LevelEditorTabManager->TryInvokeTab(FRewindDebuggerModule::DetailsTabName, bInvokeAsInactive); if (DetailsTab.IsValid()) { UpdateDetailsPanel(DetailsTab.ToSharedRef()); } } } void FRewindDebugger::UpdateDetailsPanel(TSharedRef DetailsTab) { if (bIsDetailsPanelOpen) { TSharedPtr DetailsView; if (SelectedTrack) { DetailsView = SelectedTrack->GetDetailsView(); } if (DetailsView) { DetailsTab->SetContent(DetailsView.ToSharedRef()); } else { static TSharedPtr 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(); 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("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("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(), FSlateIcon("RewindDebuggerStyle", "RewindDebugger.FirstFrame.small"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.PreviousFrame, FText(), TAttribute(), FSlateIcon("RewindDebuggerStyle", "RewindDebugger.PreviousFrame.small"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.ReversePlay, FText(), TAttribute(), 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) .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(), FSlateIcon("RewindDebuggerStyle", "RewindDebugger.NextFrame.small"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.LastFrame, FText(), TAttribute(), FSlateIcon("RewindDebuggerStyle", "RewindDebugger.LastFrame.small"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.StartRecording, FText(), TAttribute(), FSlateIcon("RewindDebuggerStyle", "RewindDebugger.StartRecording.small"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.StopRecording, FText(), TAttribute(), FSlateIcon("RewindDebuggerStyle", "RewindDebugger.StopRecording.small"))); Section.AddSeparator(NAME_None); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.AttachToSession, FText(), TAttribute(), FSlateIcon("RewindDebuggerStyle", "RewindDebugger.ConnectToSession"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.OpenTrace, FText(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.FolderOpen"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.SaveTrace, FText(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Save"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.ClearTrace, FText(), TAttribute(), FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete"))); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.AutoEject, FText(), TAttribute(), FSlateIcon("RewindDebuggerStyle", "RewindDebugger.AutoEject"))); Section.AddSeparator(NAME_None); Section.AddEntry(FToolMenuEntry::InitToolBarButton( Commands.AutoRecord, FText(), TAttribute(), 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 InSelectedTrack) { if (!InSelectedTrack.IsValid()) { return; } SelectedTrack = InSelectedTrack; SelectedTrack->HandleDoubleClick(); } TSharedPtr FRewindDebugger::BuildTrackContextMenu() const { URewindDebuggerTrackContextMenuContext* MenuContext = NewObject(); 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("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 FRewindDebugger::GetSelectedObject() const { if (SelectedTrack.IsValid()) { if (!SelectedObject.IsValid()) { SelectedObject = MakeShared(); } SelectedObject->Id = SelectedTrack->GetAssociatedObjectId(); SelectedObject->ObjectName = SelectedTrack->GetDisplayName().ToString(); return SelectedObject; } return TSharedPtr(); } TSharedPtr FRewindDebugger::GetSelectedTrack() const { return SelectedTrack; } // build a tree that's compatible with the public api from 5.0 for GetDebuggedObjects. void FRewindDebugger::RefreshDebuggedObjects(TArray>& InTracks, TArray>& OutObjects) { OutObjects.SetNum(0, EAllowShrinking::No); for (const TSharedPtr& Track : InTracks) { const int Index = OutObjects.Add(MakeShared(Track->GetAssociatedObjectId(), Track->GetDisplayName().ToString())); TArray> TrackChildren; Track->IterateSubTracks([&TrackChildren](const TSharedPtr& Child) { TrackChildren.Add(Child); }); RefreshDebuggedObjects(TrackChildren, OutObjects[Index]->Children); } } TArray>& FRewindDebugger::GetDebuggedObjects() { RefreshDebuggedObjects(DebugTracks, DebuggedObjects); return DebuggedObjects; } bool FRewindDebugger::IsObjectCurrentlyDebugged(uint64 ObjectId) const { for (const TSharedPtr& Track : DebugTracks) { if (Track->GetUObjectId() == ObjectId) { return true; } bool Found = false; Track->IterateSubTracks([ObjectId, &Found](const TSharedPtr& Child) { if (Child->GetUObjectId() == ObjectId) { Found = true; // @Todo STDBG: want to stop iteration here } }); if (Found) { return true; } } return false; } #undef LOCTEXT_NAMESPACE