// Copyright Epic Games, Inc. All Rights Reserved. #include "SFrameTrack.h" #include "Fonts/FontMeasure.h" #include "Fonts/SlateFontInfo.h" #include "Framework/Application/SlateApplication.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "HAL/PlatformTime.h" #include "Rendering/DrawElements.h" #include "Styling/CoreStyle.h" #include "TraceServices/AnalysisService.h" #include "Widgets/Layout/SScrollBar.h" // Insights #include "Insights/Common/PaintUtils.h" #include "Insights/Common/Stopwatch.h" #include "Insights/Common/TimeUtils.h" #include "Insights/InsightsManager.h" #include "Insights/InsightsStyle.h" #include "Insights/TimingProfilerCommon.h" #include "Insights/TimingProfilerManager.h" #include "Insights/ViewModels/FrameTrackHelper.h" #include "Insights/Widgets/STimingProfilerWindow.h" #include "Insights/Widgets/STimingView.h" #include //////////////////////////////////////////////////////////////////////////////////////////////////// #define LOCTEXT_NAMESPACE "SFrameTrack" //////////////////////////////////////////////////////////////////////////////////////////////////// SFrameTrack::SFrameTrack() { Reset(); } //////////////////////////////////////////////////////////////////////////////////////////////////// SFrameTrack::~SFrameTrack() { } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::Reset() { Viewport.Reset(); FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); ViewportX.SetScaleLimits(0.0001f, 16.0f); // 10000 [sample/px] to 16 [px/sample] ViewportX.SetScale(16.0f); FAxisViewportDouble& ViewportY = Viewport.GetVerticalAxisViewport(); ViewportY.SetScaleLimits(0.01, 1000000.0); ViewportY.SetScale(1500.0); bIsViewportDirty = true; SeriesMap.Reset(); SeriesOrder.Reset(); SeriesOrder.Add(TraceFrameType_Game); SeriesOrder.Add(TraceFrameType_Rendering); bIsStateDirty = true; bShowGameFrames = true; bShowRenderingFrames = true; bIsAutoZoomEnabled = true; AutoZoomViewportPos = ViewportX.GetPos(); AutoZoomViewportScale = ViewportX.GetScale(); AutoZoomViewportSize = 0.0f; AnalysisSyncNextTimestamp = 0; MousePosition = FVector2D::ZeroVector; MousePositionOnButtonDown = FVector2D::ZeroVector; ViewportPosXOnButtonDown = 0.0f; MousePositionOnButtonUp = FVector2D::ZeroVector; bIsLMB_Pressed = false; bIsRMB_Pressed = false; bIsScrolling = false; HoveredSample.Reset(); TooltipDesiredOpacity = 0.9f; TooltipOpacity = 0.0f; // ThisGeometry CursorType = ECursorType::Default; NumUpdatedFrames = 0; UpdateDurationHistory.Reset(); DrawDurationHistory.Reset(); OnPaintDurationHistory.Reset(); LastOnPaintTime = FPlatformTime::Cycles64(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::Construct(const FArguments& InArgs) { ChildSlot [SNew(SOverlay) .Visibility(EVisibility::SelfHitTestInvisible) + SOverlay::Slot() .VAlign(VAlign_Top) .Padding(FMargin(0, 0, 0, 0)) [SAssignNew(HorizontalScrollBar, SScrollBar) .Orientation(Orient_Horizontal) .AlwaysShowScrollbar(false) .Visibility(EVisibility::Visible) .Thickness(FVector2D(5.0f, 5.0f)) .RenderOpacity(0.75) .OnUserScrolled(this, &SFrameTrack::HorizontalScrollBar_OnUserScrolled)]]; UpdateHorizontalScrollBar(); BindCommands(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (ThisGeometry != AllottedGeometry || bIsViewportDirty) { bIsViewportDirty = false; const float ViewWidth = AllottedGeometry.GetLocalSize().X; const float ViewHeight = AllottedGeometry.GetLocalSize().Y; Viewport.SetSize(ViewWidth, ViewHeight); bIsStateDirty = true; } ThisGeometry = AllottedGeometry; FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); if (!bIsScrolling) { // Elastic snap to horizontal limits. if (ViewportX.UpdatePosWithinLimits()) { bIsStateDirty = true; } } // Disable auto-zoom if viewport's position or scale has changed. if (AutoZoomViewportPos != ViewportX.GetPos() || AutoZoomViewportScale != ViewportX.GetScale()) { bIsAutoZoomEnabled = false; } // Update auto-zoom if viewport size has changed. bool bAutoZoom = bIsAutoZoomEnabled && AutoZoomViewportSize != ViewportX.GetSize(); const uint64 Time = FPlatformTime::Cycles64(); if (Time > AnalysisSyncNextTimestamp) { const uint64 WaitTime = static_cast(0.1 / FPlatformTime::GetSecondsPerCycle64()); // 100ms AnalysisSyncNextTimestamp = Time + WaitTime; TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::IFrameProvider& FramesProvider = Trace::ReadFrameProvider(*Session.Get()); for (int32 FrameType = 0; FrameType < TraceFrameType_Count; ++FrameType) { TSharedPtr SeriesPtr = FindOrAddSeries(FrameType); int32 NumFrames = FramesProvider.GetFrameCount(static_cast(FrameType)); if (NumFrames > ViewportX.GetMaxValue()) { ViewportX.SetMinMaxInterval(0, NumFrames); UpdateHorizontalScrollBar(); bIsStateDirty = true; if (bIsAutoZoomEnabled) { bAutoZoom = true; } } } } } if (bAutoZoom) { AutoZoom(); } if (bIsStateDirty) { bIsStateDirty = false; UpdateState(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedRef SFrameTrack::FindOrAddSeries(int32 FrameType) { TSharedPtr* SeriesPtrPtr = SeriesMap.Find(FrameType); if (SeriesPtrPtr) { ensure((**SeriesPtrPtr).FrameType == FrameType); return (*SeriesPtrPtr).ToSharedRef(); } else { TSharedRef SeriesRef = MakeShared(FrameType); SeriesMap.Add(FrameType, SeriesRef); return SeriesRef; } } //////////////////////////////////////////////////////////////////////////////////////////////////// TSharedPtr SFrameTrack::FindSeries(int32 FrameType) const { const TSharedPtr* SeriesPtrPtr = SeriesMap.Find(FrameType); if (SeriesPtrPtr) { ensure((**SeriesPtrPtr).FrameType == FrameType); return *SeriesPtrPtr; } else { return nullptr; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::UpdateState() { FStopwatch Stopwatch; Stopwatch.Start(); // Reset stats. for (TPair>& KeyValuePair: SeriesMap) { TSharedPtr& SeriesPtr = KeyValuePair.Value; SeriesPtr->NumAggregatedFrames = 0; } NumUpdatedFrames = 0; TSharedPtr Session = FInsightsManager::Get()->GetSession(); if (Session.IsValid()) { Trace::FAnalysisSessionReadScope SessionReadScope(*Session.Get()); const Trace::IFrameProvider& FramesProvider = Trace::ReadFrameProvider(*Session.Get()); const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const uint64 StartIndex = static_cast(FMath::Max(0, ViewportX.GetValueAtOffset(0.0f))); const uint64 EndIndex = static_cast(ViewportX.GetValueAtOffset(ViewportX.GetSize())); for (int32 FrameType = 0; FrameType < TraceFrameType_Count; ++FrameType) { TSharedPtr SeriesPtr = FindOrAddSeries(FrameType); FFrameTrackSeriesBuilder Builder(*SeriesPtr, Viewport); FramesProvider.EnumerateFrames(static_cast(FrameType), StartIndex, EndIndex, [&Builder](const Trace::FFrame& Frame) { Builder.AddFrame(Frame); }); NumUpdatedFrames += Builder.GetNumAddedFrames(); } } Stopwatch.Stop(); UpdateDurationHistory.AddValue(Stopwatch.AccumulatedTime); } //////////////////////////////////////////////////////////////////////////////////////////////////// FFrameTrackSampleRef SFrameTrack::GetSampleAtMousePosition(float X, float Y) { if (!bIsStateDirty) { float SampleW = Viewport.GetSampleWidth(); int32 SampleIndex = FMath::FloorToInt(X / SampleW); if (SampleIndex >= 0) { // Search in reverse paint order. for (int32 SeriesIndex = SeriesOrder.Num() - 1; SeriesIndex >= 0; --SeriesIndex) { int32 FrameType = SeriesOrder[SeriesIndex]; if ((FrameType == TraceFrameType_Rendering && !bShowRenderingFrames) || (FrameType == TraceFrameType_Game && !bShowGameFrames)) { continue; } const TSharedPtr SeriesPtr = FindSeries(FrameType); if (SeriesPtr.IsValid()) { if (SeriesPtr->NumAggregatedFrames > 0 && SampleIndex < SeriesPtr->Samples.Num()) { const FFrameTrackSample& Sample = SeriesPtr->Samples[SampleIndex]; if (Sample.NumFrames > 0) { const FAxisViewportDouble& ViewportY = Viewport.GetVerticalAxisViewport(); const float ViewHeight = FMath::RoundToFloat(Viewport.GetHeight()); const float BaselineY = FMath::RoundToFloat(ViewportY.GetOffsetForValue(0.0)); float ValueY; if (Sample.LargestFrameDuration == std::numeric_limits::infinity()) { ValueY = ViewHeight; } else { ValueY = FMath::RoundToFloat(ViewportY.GetOffsetForValue(Sample.LargestFrameDuration)); } constexpr float ToleranceY = 3.0f; // [pixels] const float BottomY = FMath::Min(ViewHeight, ViewHeight - BaselineY + ToleranceY); const float TopY = FMath::Max(0.0f, ViewHeight - ValueY - ToleranceY); if (Y >= TopY && Y < BottomY) { return FFrameTrackSampleRef(SeriesPtr, MakeShared(Sample)); } } } } } } } return FFrameTrackSampleRef(nullptr, nullptr); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::SelectFrameAtMousePosition(float X, float Y) { FFrameTrackSampleRef SampleRef = GetSampleAtMousePosition(X, Y); if (!SampleRef.IsValid()) { SampleRef = GetSampleAtMousePosition(X - 1.0f, Y); } if (!SampleRef.IsValid()) { SampleRef = GetSampleAtMousePosition(X + 1.0f, Y); } if (SampleRef.IsValid()) { TSharedPtr Window = FTimingProfilerManager::Get()->GetProfilerWindow(); if (Window.IsValid()) { TSharedPtr TimingView = Window->GetTimingView(); if (TimingView.IsValid()) { const double StartTime = SampleRef.Sample->LargestFrameStartTime; const double Duration = SampleRef.Sample->LargestFrameDuration; TimingView->CenterOnTimeInterval(StartTime, Duration); TimingView->SelectTimeInterval(StartTime, Duration); FSlateApplication::Get().SetKeyboardFocus(TimingView); } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// int32 SFrameTrack::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const { const bool bEnabled = ShouldBeEnabled(bParentEnabled); const ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect; FDrawContext DrawContext(AllottedGeometry, MyCullingRect, InWidgetStyle, DrawEffects, OutDrawElements, LayerId); const TSharedRef FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); FSlateFontInfo SummaryFont = FCoreStyle::GetDefaultFontStyle("Regular", 8); const FSlateBrush* WhiteBrush = FInsightsStyle::Get().GetBrush("WhiteBrush"); const float ViewWidth = AllottedGeometry.Size.X; const float ViewHeight = AllottedGeometry.Size.Y; int32 NumDrawSamples = 0; ////////////////////////////////////////////////// { FStopwatch Stopwatch; Stopwatch.Start(); FFrameTrackDrawHelper Helper(DrawContext, Viewport); Helper.DrawBackground(); // Draw the horizontal axis grid. DrawHorizontalAxisGrid(DrawContext, WhiteBrush, SummaryFont); // Draw frames, for each visible Series. for (int32 SeriesIndex = 0; SeriesIndex < SeriesOrder.Num(); ++SeriesIndex) { int32 FrameType = SeriesOrder[SeriesIndex]; if ((FrameType == TraceFrameType_Rendering && !bShowRenderingFrames) || (FrameType == TraceFrameType_Game && !bShowGameFrames)) { continue; } const TSharedPtr SeriesPtr = FindSeries(FrameType); if (SeriesPtr.IsValid()) { Helper.DrawCached(*SeriesPtr); } } NumDrawSamples = Helper.GetNumDrawSamples(); // Highlight the mouse hovered sample (frame). if (HoveredSample.IsValid()) { Helper.DrawHoveredSample(*HoveredSample.Sample); } // Draw the vertical axis grid. DrawVerticalAxisGrid(DrawContext, WhiteBrush, SummaryFont); // Draw tooltip for hovered sample (frame). if (HoveredSample.IsValid()) { if (TooltipOpacity < TooltipDesiredOpacity) { TooltipOpacity = TooltipOpacity * 0.9f + TooltipDesiredOpacity * 0.1f; } else { TooltipOpacity = TooltipDesiredOpacity; } const FString Text = FString::Format(TEXT("{0} frame {1} ({2})"), {FFrameTrackDrawHelper::FrameTypeToString(HoveredSample.Series->FrameType), FText::AsNumber(HoveredSample.Sample->LargestFrameIndex).ToString(), TimeUtils::FormatTimeAuto(HoveredSample.Sample->LargestFrameDuration)}); FVector2D TextSize = FontMeasureService->Measure(Text, SummaryFont); const float DX = 2.0f; const float W2 = TextSize.X / 2 + DX; const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); float X1 = ViewportX.GetOffsetForValue(HoveredSample.Sample->LargestFrameIndex); float CX = X1 + FMath::RoundToFloat(Viewport.GetSampleWidth() / 2); if (CX + W2 > ViewportX.GetSize()) { CX = FMath::RoundToFloat(ViewportX.GetSize() - W2); } if (CX - W2 < 0) { CX = W2; } const float Y = 10.0f; const float H = 14.0f; DrawContext.DrawBox(CX - W2, Y, 2 * W2, H, WhiteBrush, FLinearColor(0.7, 0.7, 0.7, TooltipOpacity)); DrawContext.LayerId++; DrawContext.DrawText(CX - W2 + DX, Y + 1.0f, Text, SummaryFont, FLinearColor(0.0, 0.0, 0.0, TooltipOpacity)); DrawContext.LayerId++; } TSharedPtr Window = FTimingProfilerManager::Get()->GetProfilerWindow(); if (Window) { TSharedPtr TimingView = Window->GetTimingView(); if (TimingView) { TSharedPtr SeriesPtr = nullptr; for (const TPair>& KeyValuePair: SeriesMap) { SeriesPtr = KeyValuePair.Value; break; // stop at first enumerated Series } if (SeriesPtr.IsValid()) { // Highlight the area corresponding to viewport of Timing View. const double StartTime = TimingView->GetViewport().GetStartTime(); const double EndTime = TimingView->GetViewport().GetEndTime(); Helper.DrawHighlightedInterval(*SeriesPtr, StartTime, EndTime); } } } Stopwatch.Stop(); DrawDurationHistory.AddValue(Stopwatch.AccumulatedTime); } ////////////////////////////////////////////////// const bool bShouldDisplayDebugInfo = FInsightsManager::Get()->IsDebugInfoEnabled(); if (bShouldDisplayDebugInfo) { const float MaxFontCharHeight = FontMeasureService->Measure(TEXT("!"), SummaryFont).Y; const float DbgDY = MaxFontCharHeight; const float DbgW = 280.0f; const float DbgH = DbgDY * 4 + 3.0f; const float DbgX = ViewWidth - DbgW - 20.0f; float DbgY = 7.0f; DrawContext.LayerId++; DrawContext.DrawBox(DbgX - 2.0f, DbgY - 2.0f, DbgW, DbgH, WhiteBrush, FLinearColor(1.0, 1.0, 1.0, 0.9)); DrawContext.LayerId++; FLinearColor DbgTextColor(0.0, 0.0, 0.0, 0.9); // Time interval since last OnPaint call. const uint64 CurrentTime = FPlatformTime::Cycles64(); const uint64 OnPaintDuration = CurrentTime - LastOnPaintTime; LastOnPaintTime = CurrentTime; OnPaintDurationHistory.AddValue(OnPaintDuration); // saved for last 32 OnPaint calls const uint64 AvgOnPaintDuration = OnPaintDurationHistory.ComputeAverage(); const uint64 AvgOnPaintDurationMs = FStopwatch::Cycles64ToMilliseconds(AvgOnPaintDuration); const double AvgOnPaintFps = AvgOnPaintDurationMs != 0 ? 1.0 / FStopwatch::Cycles64ToSeconds(AvgOnPaintDuration) : 0.0; const uint64 AvgUpdateDurationMs = FStopwatch::Cycles64ToMilliseconds(UpdateDurationHistory.ComputeAverage()); const uint64 AvgDrawDurationMs = FStopwatch::Cycles64ToMilliseconds(DrawDurationHistory.ComputeAverage()); // Draw performance info. DrawContext.DrawText( DbgX, DbgY, FString::Printf(TEXT("U: %llu ms, D: %llu ms + %llu ms = %llu ms (%d fps)"), AvgUpdateDurationMs, // caching time AvgDrawDurationMs, // drawing time AvgOnPaintDurationMs - AvgDrawDurationMs, // other overhead to OnPaint calls AvgOnPaintDurationMs, // average time between two OnPaint calls FMath::RoundToInt(AvgOnPaintFps)), // framerate of OnPaint calls SummaryFont, DbgTextColor); DbgY += DbgDY; // Draw number of draw calls. DrawContext.DrawText( DbgX, DbgY, FString::Printf(TEXT("U: %s frames, D: %s samples"), *FText::AsNumber(NumUpdatedFrames).ToString(), *FText::AsNumber(NumDrawSamples).ToString()), SummaryFont, DbgTextColor); DbgY += DbgDY; // Draw viewport's horizontal info. DrawContext.DrawText( DbgX, DbgY, Viewport.GetHorizontalAxisViewport().ToDebugString(TEXT("X"), TEXT("frame")), SummaryFont, DbgTextColor); DbgY += DbgDY; // Draw viewport's vertical info. DrawContext.DrawText( DbgX, DbgY, Viewport.GetVerticalAxisViewport().ToDebugString(TEXT("Y")), SummaryFont, DbgTextColor); DbgY += DbgDY; } return SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled && IsEnabled()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::DrawVerticalAxisGrid(FDrawContext& DrawContext, const FSlateBrush* Brush, const FSlateFontInfo& Font) const { const float ViewWidth = Viewport.GetWidth(); const FAxisViewportDouble& ViewportY = Viewport.GetVerticalAxisViewport(); const float RoundedViewHeight = FMath::RoundToFloat(ViewportY.GetSize()); constexpr float TextH = 14.0f; constexpr float MinDY = 12.0f; // min vertical distance between horizontal grid lines const FLinearColor GridColor(0.0f, 0.0f, 0.0f, 0.1f); const FLinearColor TextBgColor(0.05f, 0.05f, 0.05f, 1.0f); // const FLinearColor TextColor(1.0f, 1.0f, 1.0f, 1.0f); const TSharedRef FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService(); const double GridValues[] = { 0.0, 1.0 / 200.0, // 5 ms (200 fps) 1.0 / 120.0, // 8.3 ms (120 fps) 1.0 / 90.0, // 11.1 ms (90 fps) 1.0 / 72.0, // 13.9 ms (72 fps) 1.0 / 60.0, // 16.7 ms (60 fps) 1.0 / 30.0, // 33.3 ms (30 fps) 1.0 / 20.0, // 50 ms (20 fps) 1.0 / 15.0, // 66.7 ms (15 fps) 1.0 / 10.0, // 100 ms (10 fps) 1.0 / 5.0, // 200 ms (5 fps) 1.0, // 1s 10.0, // 10s 60.0, // 1m 600.0, // 10m 3600.0 // 1h }; constexpr int32 NumGridValues = sizeof(GridValues) / sizeof(double); double PreviousY = -MinDY; for (int32 Index = 0; Index < NumGridValues; ++Index) { const double Value = GridValues[Index]; constexpr double Time60fps = 1.0 / 60.0; constexpr double Time30fps = 1.0 / 30.0; FLinearColor TextColor; if (Value <= Time60fps) { TextColor = FLinearColor(0.5f, 1.0f, 0.5f, 1.0f); } else if (Value <= Time30fps) { TextColor = FLinearColor(1.0f, 1.0f, 0.5f, 1.0f); // const float U = (Value - Time60fps) * 0.5 / (Time30fps - Time60fps); // TextColor = FLinearColor(0.5f + U, 1.0f - U, 0.5f, 1.0f); } else { TextColor = FLinearColor(1.0f, 0.5f, 0.5f, 1.0f); } const float Y = RoundedViewHeight - FMath::RoundToFloat(ViewportY.GetOffsetForValue(Value)); if (Y < 0) { break; } if (Y > RoundedViewHeight + TextH || FMath::Abs(PreviousY - Y) < MinDY) { continue; } PreviousY = Y; // Draw horizontal grid line. DrawContext.DrawBox(0, Y, ViewWidth, 1, Brush, GridColor); const FString LabelText = (Value == 0.0) ? TEXT("0") : (Value <= 1.0) ? FString::Printf(TEXT("%s (%.0f fps)"), *TimeUtils::FormatTimeAuto(Value), 1.0 / Value) : TimeUtils::FormatTimeAuto(Value); const FVector2D LabelTextSize = FontMeasureService->Measure(LabelText, Font); float LabelX = ViewWidth - LabelTextSize.X - 4.0f; float LabelY = FMath::Clamp(Y - TextH / 2, 0.0f, RoundedViewHeight - TextH); // Draw background for value text. DrawContext.DrawBox(LabelX, LabelY, LabelTextSize.X + 4.0f, TextH, Brush, TextBgColor); // Draw value text. DrawContext.DrawText(LabelX + 2.0f, LabelY + 1.0f, LabelText, Font, TextColor); } DrawContext.LayerId++; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::DrawHorizontalAxisGrid(FDrawContext& DrawContext, const FSlateBrush* Brush, const FSlateFontInfo& Font) const { const FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const float RoundedViewWidth = FMath::RoundToFloat(ViewportX.GetSize()); constexpr float MinDX = 125.0f; // min horizontal distance between vertical grid lines const int32 LeftIndex = ViewportX.GetValueAtOffset(0.0f); const int32 GridIndex = ViewportX.GetValueAtOffset(MinDX); const int32 RightIndex = ViewportX.GetValueAtOffset(RoundedViewWidth); const int32 Delta = GridIndex - LeftIndex; if (Delta > 0) { // Compute rounding based on magnitude of visible range of samples (Delta). int32 Power10 = 1; int32 Delta10 = Delta; while (Delta10 > 0) { Delta10 /= 10; Power10 *= 10; } if (Power10 >= 100) { Power10 /= 100; } else { Power10 = 1; } const int32 Grid = ((Delta + Power10 - 1) / Power10) * Power10; // next value divisible with a multiple of 10 // Skip grid lines for negative indices. double StartIndex = ((LeftIndex + Grid - 1) / Grid) * Grid; while (StartIndex < 0) { StartIndex += Grid; } const float ViewHeight = Viewport.GetHeight(); const FLinearColor GridColor(0.0f, 0.0f, 0.0f, 0.1f); const FLinearColor TopTextColor(1.0f, 1.0f, 1.0f, 0.7f); for (int32 Index = StartIndex; Index < RightIndex; Index += Grid) { const float X = FMath::RoundToFloat(ViewportX.GetOffsetForValue(Index)); // Draw vertical grid line. DrawContext.DrawBox(X, 0, 1, ViewHeight, Brush, GridColor); // Draw label. const FString LabelText = FText::AsNumber(Index).ToString(); DrawContext.DrawText(X + 2.0f, 10.0f, LabelText, Font, TopTextColor); } DrawContext.LayerId++; } } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SFrameTrack::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { FReply Reply = FReply::Unhandled(); MousePositionOnButtonDown = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); ViewportPosXOnButtonDown = Viewport.GetHorizontalAxisViewport().GetPos(); if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { bIsLMB_Pressed = true; // Capture mouse. Reply = FReply::Handled().CaptureMouse(SharedThis(this)); } else if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { bIsRMB_Pressed = true; // Capture mouse, so we can scroll outside this widget. Reply = FReply::Handled().CaptureMouse(SharedThis(this)); } return Reply; } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SFrameTrack::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { FReply Reply = FReply::Unhandled(); MousePositionOnButtonUp = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); const bool bIsValidForMouseClick = MousePositionOnButtonUp.Equals(MousePositionOnButtonDown, MOUSE_SNAP_DISTANCE); if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { if (bIsLMB_Pressed) { if (bIsScrolling) { bIsScrolling = false; CursorType = ECursorType::Default; } else if (bIsValidForMouseClick) { SelectFrameAtMousePosition(MousePositionOnButtonUp.X, MousePositionOnButtonUp.Y); } bIsLMB_Pressed = false; // Release the mouse. Reply = FReply::Handled().ReleaseMouseCapture(); } } else if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { if (bIsRMB_Pressed) { if (bIsScrolling) { bIsScrolling = false; CursorType = ECursorType::Default; } else if (bIsValidForMouseClick) { ShowContextMenu(MouseEvent); } bIsRMB_Pressed = false; // Release mouse as we no longer scroll. Reply = FReply::Handled().ReleaseMouseCapture(); } } return Reply; } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SFrameTrack::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { FReply Reply = FReply::Unhandled(); MousePosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); if (!MouseEvent.GetCursorDelta().IsZero()) { if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton) || MouseEvent.IsMouseButtonDown(EKeys::RightMouseButton)) { if (HasMouseCapture()) { if (!bIsScrolling) { bIsScrolling = true; CursorType = ECursorType::Hand; HoveredSample.Reset(); } FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); const float PosX = ViewportPosXOnButtonDown + (MousePositionOnButtonDown.X - MousePosition.X); ViewportX.ScrollAtValue(ViewportX.GetValueAtPos(PosX)); // align viewport position with sample (frame index) UpdateHorizontalScrollBar(); bIsStateDirty = true; } } else { if (!HoveredSample.IsValid()) { TooltipOpacity = 0.0f; } HoveredSample = GetSampleAtMousePosition(MousePosition.X, MousePosition.Y); if (!HoveredSample.IsValid()) { HoveredSample = GetSampleAtMousePosition(MousePosition.X - 1.0f, MousePosition.Y); } if (!HoveredSample.IsValid()) { HoveredSample = GetSampleAtMousePosition(MousePosition.X + 1.0f, MousePosition.Y); } } Reply = FReply::Handled(); } return Reply; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::OnMouseLeave(const FPointerEvent& MouseEvent) { if (!HasMouseCapture()) { bIsLMB_Pressed = false; bIsRMB_Pressed = false; HoveredSample.Reset(); CursorType = ECursorType::Default; } } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SFrameTrack::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { MousePosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()); if (MouseEvent.GetModifierKeys().IsShiftDown()) { FAxisViewportDouble& ViewportY = Viewport.GetVerticalAxisViewport(); // Zoom in/out vertically. const float Delta = MouseEvent.GetWheelDelta(); constexpr float ZoomStep = 0.25f; // as percent float ScaleY; if (Delta > 0) { ScaleY = ViewportY.GetScale() * FMath::Pow(1.0f + ZoomStep, Delta); } else { ScaleY = ViewportY.GetScale() * FMath::Pow(1.0f / (1.0f + ZoomStep), -Delta); } ViewportY.SetScale(ScaleY); // UpdateVerticalScrollBar(); } else // if (MouseEvent.GetModifierKeys().IsControlDown()) { // Zoom in/out horizontally. const float Delta = MouseEvent.GetWheelDelta(); ZoomHorizontally(Delta, MousePosition.X); } return FReply::Handled(); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::ZoomHorizontally(const float Delta, const float X) { FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); ViewportX.RelativeZoomWithFixedOffset(Delta, X); ViewportX.ScrollAtValue(ViewportX.GetValueAtPos(ViewportX.GetPos())); // align viewport position with sample (frame index) UpdateHorizontalScrollBar(); bIsStateDirty = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// FReply SFrameTrack::OnMouseButtonDoubleClick(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { return FReply::Unhandled(); } //////////////////////////////////////////////////////////////////////////////////////////////////// FCursorReply SFrameTrack::OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const { FCursorReply CursorReply = FCursorReply::Unhandled(); if (CursorType == ECursorType::Arrow) { CursorReply = FCursorReply::Cursor(EMouseCursor::ResizeLeftRight); } else if (CursorType == ECursorType::Hand) { CursorReply = FCursorReply::Cursor(EMouseCursor::GrabHand); } return CursorReply; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::ShowContextMenu(const FPointerEvent& MouseEvent) { const bool bShouldCloseWindowAfterMenuSelection = true; FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, NULL); MenuBuilder.BeginSection("Series", LOCTEXT("ContextMenu_Header_Series", "Series")); { struct FLocal { static bool ReturnFalse() { return false; } }; FUIAction Action_ShowGameFrames( FExecuteAction::CreateSP(this, &SFrameTrack::ContextMenu_ShowGameFrames_Execute), FCanExecuteAction::CreateSP(this, &SFrameTrack::ContextMenu_ShowGameFrames_CanExecute), FIsActionChecked::CreateSP(this, &SFrameTrack::ContextMenu_ShowGameFrames_IsChecked)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_ShowGameFrames", "Game Frames"), LOCTEXT("ContextMenu_ShowGameFrames_Desc", "Show/hide the Game frames"), FSlateIcon(), Action_ShowGameFrames, NAME_None, EUserInterfaceActionType::ToggleButton); FUIAction Action_ShowRenderingFrames( FExecuteAction::CreateSP(this, &SFrameTrack::ContextMenu_ShowRenderingFrames_Execute), FCanExecuteAction::CreateSP(this, &SFrameTrack::ContextMenu_ShowRenderingFrames_CanExecute), FIsActionChecked::CreateSP(this, &SFrameTrack::ContextMenu_ShowRenderingFrames_IsChecked)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_ShowRenderingFrames", "Rendering Frames"), LOCTEXT("ContextMenu_ShowRenderingFrames_Desc", "Show/hide the Rendering frames"), FSlateIcon(), Action_ShowRenderingFrames, NAME_None, EUserInterfaceActionType::ToggleButton); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("Misc"); { FUIAction Action_AutoZoom( FExecuteAction::CreateSP(this, &SFrameTrack::ContextMenu_AutoZoom_Execute), FCanExecuteAction::CreateSP(this, &SFrameTrack::ContextMenu_AutoZoom_CanExecute), FIsActionChecked::CreateSP(this, &SFrameTrack::ContextMenu_AutoZoom_IsChecked)); MenuBuilder.AddMenuEntry( LOCTEXT("ContextMenu_AutoZoom", "Auto Zoom"), LOCTEXT("ContextMenu_AutoZoom_Desc", "Enable auto zoom. Makes entire session time range to fit into view."), FSlateIcon(), Action_AutoZoom, NAME_None, EUserInterfaceActionType::ToggleButton); } MenuBuilder.EndSection(); TSharedRef MenuWidget = MenuBuilder.MakeWidget(); FWidgetPath EventPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath(); const FVector2D ScreenSpacePosition = MouseEvent.GetScreenSpacePosition(); FSlateApplication::Get().PushMenu(SharedThis(this), EventPath, MenuWidget, ScreenSpacePosition, FPopupTransitionEffect::ContextMenu); } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::ContextMenu_ShowGameFrames_Execute() { bShowGameFrames = !bShowGameFrames; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SFrameTrack::ContextMenu_ShowGameFrames_CanExecute() { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SFrameTrack::ContextMenu_ShowGameFrames_IsChecked() { return bShowGameFrames; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::ContextMenu_ShowRenderingFrames_Execute() { bShowRenderingFrames = !bShowRenderingFrames; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SFrameTrack::ContextMenu_ShowRenderingFrames_CanExecute() { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SFrameTrack::ContextMenu_ShowRenderingFrames_IsChecked() { return bShowRenderingFrames; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::ContextMenu_AutoZoom_Execute() { bIsAutoZoomEnabled = !bIsAutoZoomEnabled; if (bIsAutoZoomEnabled) { AutoZoom(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SFrameTrack::ContextMenu_AutoZoom_CanExecute() { return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool SFrameTrack::ContextMenu_AutoZoom_IsChecked() { return bIsAutoZoomEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::AutoZoom() { FAxisViewportInt32& ViewportX = Viewport.GetHorizontalAxisViewport(); AutoZoomViewportPos = ViewportX.GetMinPos(); ViewportX.ScrollAtPos(AutoZoomViewportPos); AutoZoomViewportSize = ViewportX.GetSize(); if (AutoZoomViewportSize > 0.0f && ViewportX.GetMaxValue() - ViewportX.GetMinValue() > 0) { float DX = ViewportX.GetMaxPos() - ViewportX.GetMinPos(); // Auto zoom in. while (DX < AutoZoomViewportSize) { const float OldScale = ViewportX.GetScale(); ViewportX.RelativeZoomWithFixedOffset(+0.1f, 0.0f); ViewportX.ScrollAtPos(AutoZoomViewportPos); DX = ViewportX.GetMaxPos() - ViewportX.GetMinPos(); if (OldScale == ViewportX.GetScale()) { break; } } // Auto zoom out (until entire session frame range fits into view). while (DX > AutoZoomViewportSize) { const float OldScale = ViewportX.GetScale(); ViewportX.RelativeZoomWithFixedOffset(-0.1f, 0.0f); ViewportX.ScrollAtPos(AutoZoomViewportPos); DX = ViewportX.GetMaxPos() - ViewportX.GetMinPos(); if (OldScale == ViewportX.GetScale()) { break; } } } AutoZoomViewportScale = ViewportX.GetScale(); UpdateHorizontalScrollBar(); bIsStateDirty = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::BindCommands() { } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::HorizontalScrollBar_OnUserScrolled(float ScrollOffset) { Viewport.GetHorizontalAxisViewport().OnUserScrolled(HorizontalScrollBar, ScrollOffset); bIsStateDirty = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void SFrameTrack::UpdateHorizontalScrollBar() { Viewport.GetHorizontalAxisViewport().UpdateScrollBar(HorizontalScrollBar); } //////////////////////////////////////////////////////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE