diff --git a/src/core/settings.cpp b/src/core/settings.cpp index e6249198d..496166d9a 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -264,6 +264,7 @@ void Settings::Load(SettingsInterface& si) display_show_resolution = si.GetBoolValue("Display", "ShowResolution", false); display_show_cpu = si.GetBoolValue("Display", "ShowCPU", false); display_show_gpu = si.GetBoolValue("Display", "ShowGPU", false); + display_show_frame_times = si.GetBoolValue("Display", "ShowFrameTimes", false); display_show_status_indicators = si.GetBoolValue("Display", "ShowStatusIndicators", true); display_show_inputs = si.GetBoolValue("Display", "ShowInputs", false); display_show_enhancements = si.GetBoolValue("Display", "ShowEnhancements", false); @@ -478,6 +479,7 @@ void Settings::Save(SettingsInterface& si) const si.SetBoolValue("Display", "ShowResolution", display_show_resolution); si.SetBoolValue("Display", "ShowCPU", display_show_cpu); si.SetBoolValue("Display", "ShowGPU", display_show_gpu); + si.SetBoolValue("Display", "ShowFrameTimes", display_show_frame_times); si.SetBoolValue("Display", "ShowStatusIndicators", display_show_status_indicators); si.SetBoolValue("Display", "ShowInputs", display_show_inputs); si.SetBoolValue("Display", "ShowEnhancements", display_show_enhancements); diff --git a/src/core/settings.h b/src/core/settings.h index 9e5305ab4..d1bc2b7a8 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -137,6 +137,7 @@ struct Settings bool display_show_resolution = false; bool display_show_cpu = false; bool display_show_gpu = false; + bool display_show_frame_times = false; bool display_show_status_indicators = true; bool display_show_inputs = false; bool display_show_enhancements = false; diff --git a/src/core/system.cpp b/src/core/system.cpp index 0317b734d..0ce6bddcf 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -155,12 +155,14 @@ static bool s_display_all_frames = true; static bool s_syncing_to_host = false; static float s_average_frame_time_accumulator = 0.0f; -static float s_worst_frame_time_accumulator = 0.0f; +static float s_minimum_frame_time_accumulator = 0.0f; +static float s_maximum_frame_time_accumulator = 0.0f; static float s_vps = 0.0f; static float s_fps = 0.0f; static float s_speed = 0.0f; -static float s_worst_frame_time = 0.0f; +static float s_minimum_frame_time = 0.0f; +static float s_maximum_frame_time = 0.0f; static float s_average_frame_time = 0.0f; static float s_cpu_thread_usage = 0.0f; static float s_cpu_thread_time = 0.0f; @@ -169,6 +171,8 @@ static float s_sw_thread_time = 0.0f; static float s_average_gpu_time = 0.0f; static float s_accumulated_gpu_time = 0.0f; static float s_gpu_usage = 0.0f; +static System::FrameTimeHistory s_frame_time_history; +static u32 s_frame_time_history_pos = 0; static u32 s_last_frame_number = 0; static u32 s_last_internal_frame_number = 0; static u32 s_last_global_tick_counter = 0; @@ -339,9 +343,13 @@ float System::GetAverageFrameTime() { return s_average_frame_time; } -float System::GetWorstFrameTime() +float System::GetMinimumFrameTime() { - return s_worst_frame_time; + return s_minimum_frame_time; +} +float System::GetMaximumFrameTime() +{ + return s_maximum_frame_time; } float System::GetThrottleFrequency() { @@ -371,6 +379,14 @@ float System::GetGPUAverageTime() { return s_average_gpu_time; } +const System::FrameTimeHistory& System::GetFrameTimeHistory() +{ + return s_frame_time_history; +} +u32 System::GetFrameTimeHistoryPos() +{ + return s_frame_time_history_pos; +} bool System::IsExeFileName(const std::string_view& path) { @@ -802,6 +818,7 @@ void System::SetDefaultSettings(SettingsInterface& si) temp.display_show_resolution = g_settings.display_show_resolution; temp.display_show_cpu = g_settings.display_show_cpu; temp.display_show_gpu = g_settings.display_show_gpu; + temp.display_show_frame_times = g_settings.display_show_frame_times; // keep controller, we reset it elsewhere for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) @@ -1305,12 +1322,14 @@ bool System::Initialize(bool force_software_renderer) s_fast_forward_enabled = false; s_average_frame_time_accumulator = 0.0f; - s_worst_frame_time_accumulator = 0.0f; + s_minimum_frame_time_accumulator = 0.0f; + s_maximum_frame_time_accumulator = 0.0f; s_vps = 0.0f; s_fps = 0.0f; s_speed = 0.0f; - s_worst_frame_time = 0.0f; + s_minimum_frame_time = 0.0f; + s_maximum_frame_time = 0.0f; s_average_frame_time = 0.0f; s_cpu_thread_usage = 0.0f; s_cpu_thread_time = 0.0f; @@ -1326,6 +1345,8 @@ bool System::Initialize(bool force_software_renderer) s_last_cpu_time = 0; s_fps_timer.Reset(); s_frame_timer.Reset(); + s_frame_time_history.fill(0.0f); + s_frame_time_history_pos = 0; TimingEvents::Initialize(); @@ -2092,8 +2113,6 @@ void System::DoRunFrame() void System::RunFrame() { - s_frame_timer.Reset(); - if (s_rewind_load_counter >= 0) { DoRewind(); @@ -2183,9 +2202,12 @@ void System::RunFrames() void System::UpdatePerformanceCounters() { - const float frame_time = static_cast(s_frame_timer.GetTimeMilliseconds()); + const float frame_time = static_cast(s_frame_timer.GetTimeMillisecondsAndReset()); + s_minimum_frame_time_accumulator = (s_minimum_frame_time_accumulator == 0.0f) ? frame_time : std::min(s_minimum_frame_time_accumulator, frame_time); s_average_frame_time_accumulator += frame_time; - s_worst_frame_time_accumulator = std::max(s_worst_frame_time_accumulator, frame_time); + s_maximum_frame_time_accumulator = std::max(s_maximum_frame_time_accumulator, frame_time); + s_frame_time_history[s_frame_time_history_pos] = frame_time; + s_frame_time_history_pos = (s_frame_time_history_pos + 1) % NUM_FRAME_TIME_SAMPLES; // update fps counter const Common::Timer::Value now_ticks = Common::Timer::GetCurrentValue(); @@ -2204,10 +2226,10 @@ void System::UpdatePerformanceCounters() const double time_divider = 1000.0 * (1.0 / static_cast(Threading::GetThreadTicksPerSecond())) * (1.0 / static_cast(frames_run)); - s_worst_frame_time = s_worst_frame_time_accumulator; - s_worst_frame_time_accumulator = 0.0f; - s_average_frame_time = s_average_frame_time_accumulator / frames_run; - s_average_frame_time_accumulator = 0.0f; + s_minimum_frame_time = std::exchange(s_minimum_frame_time_accumulator, 0.0f); + s_average_frame_time = std::exchange(s_average_frame_time_accumulator, 0.0f) / frames_run; + s_maximum_frame_time = std::exchange(s_maximum_frame_time_accumulator, 0.0f); + s_vps = static_cast(frames_run / time); s_last_frame_number = s_frame_number; s_fps = static_cast(s_internal_frame_number - s_last_internal_frame_number) / time; @@ -2240,8 +2262,8 @@ void System::UpdatePerformanceCounters() s_accumulated_gpu_time = 0.0f; s_presents_since_last_update = 0; - Log_VerbosePrintf("FPS: %.2f VPS: %.2f CPU: %.2f GPU: %.2f Average: %.2fms Worst: %.2fms", s_fps, s_vps, - s_cpu_thread_usage, s_gpu_usage, s_average_frame_time, s_worst_frame_time); + Log_VerbosePrintf("FPS: %.2f VPS: %.2f CPU: %.2f GPU: %.2f Average: %.2fms Min: %.2fms Max: %.2f ms", s_fps, s_vps, + s_cpu_thread_usage, s_gpu_usage, s_average_frame_time, s_minimum_frame_time, s_maximum_frame_time); Host::OnPerformanceCountersUpdated(); } @@ -2258,7 +2280,9 @@ void System::ResetPerformanceCounters() s_last_sw_time = 0; s_average_frame_time_accumulator = 0.0f; - s_worst_frame_time_accumulator = 0.0f; + s_minimum_frame_time_accumulator = 0.0f; + s_maximum_frame_time_accumulator = 0.0f; + s_frame_timer.Reset(); s_fps_timer.Reset(); ResetThrottler(); } @@ -3491,8 +3515,6 @@ void System::SetRewinding(bool enabled) void System::DoRewind() { - s_frame_timer.Reset(); - if (s_rewind_load_counter == 0) { const u32 skip_saves = BoolToUInt32(!s_rewinding_first_save); diff --git a/src/core/system.h b/src/core/system.h index 4ac181715..614bbb90e 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -176,11 +176,15 @@ const std::string& GetRunningTitle(); bool IsRunningBIOS(); // TODO: Move to PerformanceMetrics +static constexpr u32 NUM_FRAME_TIME_SAMPLES = 150; +using FrameTimeHistory = std::array; + float GetFPS(); float GetVPS(); float GetEmulationSpeed(); float GetAverageFrameTime(); -float GetWorstFrameTime(); +float GetMinimumFrameTime(); +float GetMaximumFrameTime(); float GetThrottleFrequency(); float GetCPUThreadUsage(); float GetCPUThreadAverageTime(); @@ -188,6 +192,8 @@ float GetSWThreadUsage(); float GetSWThreadAverageTime(); float GetGPUUsage(); float GetGPUAverageTime(); +const FrameTimeHistory& GetFrameTimeHistory(); +u32 GetFrameTimeHistoryPos(); /// Loads global settings (i.e. EmuConfig). void LoadSettings(bool display_osd_messages); diff --git a/src/duckstation-qt/advancedsettingswidget.cpp b/src/duckstation-qt/advancedsettingswidget.cpp index b584d4692..5a8ca72ff 100644 --- a/src/duckstation-qt/advancedsettingswidget.cpp +++ b/src/duckstation-qt/advancedsettingswidget.cpp @@ -226,6 +226,8 @@ void AdvancedSettingsWidget::addTweakOptions() "DisableAllEnhancements", false); addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Show Status Indicators"), "Display", "ShowStatusIndicators", true); + addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Show Frame Times"), "Display", + "ShowFrameTimes", false); addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Apply Compatibility Settings"), "Main", "ApplyCompatibilitySettings", true); addIntRangeTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Display FPS Limit"), "Display", "MaxFPS", 0, 1000, 0); @@ -303,6 +305,7 @@ void AdvancedSettingsWidget::onResetToDefaultClicked() setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Disable all enhancements setBooleanTweakOption(m_ui.tweakOptionTable, i++, true); // Show status indicators + setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Show frame times setBooleanTweakOption(m_ui.tweakOptionTable, i++, true); // Apply compatibility settings setIntRangeTweakOption(m_ui.tweakOptionTable, i++, 0); // Display FPS limit setChoiceTweakOption(m_ui.tweakOptionTable, i++, 0); // Multisample antialiasing diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 15dd37396..6ef7cb4d2 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -2661,6 +2661,9 @@ void FullscreenUI::DrawInterfaceSettingsPage() "ShowCPU", false); DrawToggleSetting(bsi, ICON_FA_SPINNER " Show GPU Usage", "Shows the host's GPU usage in the top-right corner of the display.", "Display", "ShowGPU", false); + DrawToggleSetting(bsi, ICON_FA_RULER_HORIZONTAL " Show Frame Times", + "Shows a visual history of frame times in the upper-left corner of the display.", "EmuCore/GS", + "ShowFrameTimes", false); DrawToggleSetting(bsi, ICON_FA_RULER_VERTICAL " Show Resolution", "Shows the current rendering resolution of the system in the top-right corner of the display.", "Display", "ShowResolution", false); diff --git a/src/frontend-common/imgui_manager.cpp b/src/frontend-common/imgui_manager.cpp index 4599fb37a..88e70eb8c 100644 --- a/src/frontend-common/imgui_manager.cpp +++ b/src/frontend-common/imgui_manager.cpp @@ -472,7 +472,7 @@ bool ImGuiManager::AddIconFonts(float size) 0xf15d, 0xf15d, 0xf188, 0xf188, 0xf191, 0xf192, 0xf1dd, 0xf1de, 0xf1e6, 0xf1e6, 0xf1eb, 0xf1eb, 0xf1f8, 0xf1f8, 0xf1fc, 0xf1fc, 0xf242, 0xf242, 0xf245, 0xf245, 0xf26c, 0xf26c, 0xf279, 0xf279, 0xf2d0, 0xf2d0, 0xf2db, 0xf2db, 0xf2f2, 0xf2f2, 0xf2f5, 0xf2f5, 0xf3c1, 0xf3c1, 0xf410, 0xf410, 0xf466, 0xf466, 0xf500, 0xf500, 0xf51f, 0xf51f, - 0xf545, 0xf545, 0xf548, 0xf548, 0xf552, 0xf552, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2, 0xf5aa, 0xf5aa, 0xf5e7, 0xf5e7, + 0xf545, 0xf545, 0xf547, 0xf548, 0xf552, 0xf552, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2, 0xf5aa, 0xf5aa, 0xf5e7, 0xf5e7, 0xf65d, 0xf65e, 0xf6a9, 0xf6a9, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815, 0xf818, 0xf818, 0xf84c, 0xf84c, 0xf8cc, 0xf8cc, 0x0, 0x0}; diff --git a/src/frontend-common/imgui_overlays.cpp b/src/frontend-common/imgui_overlays.cpp index f8df8e78a..ae3bd0419 100644 --- a/src/frontend-common/imgui_overlays.cpp +++ b/src/frontend-common/imgui_overlays.cpp @@ -4,6 +4,7 @@ #include "imgui_overlays.h" #include "IconsFontAwesome5.h" #include "common/assert.h" +#include "common/align.h" #include "common/file_system.h" #include "common/log.h" #include "common/string_util.h" @@ -27,6 +28,7 @@ #include "imgui_manager.h" #include "input_manager.h" #include "util/audio_stream.h" +#include "gsl/span" #include #include #include @@ -34,6 +36,16 @@ #include #include +#if defined(CPU_X64) +#include +#elif defined(CPU_AARCH64) +#ifdef _MSC_VER +#include +#else +#include +#endif +#endif + Log_SetChannel(ImGuiManager); namespace ImGuiManager { @@ -47,6 +59,74 @@ namespace SaveStateSelectorUI { static void Draw(); } +static std::tuple GetMinMax(gsl::span values) +{ +#if defined(CPU_X64) + __m128 vmin(_mm_loadu_ps(values.data())); + __m128 vmax(vmin); + + const u32 count = static_cast(values.size()); + const u32 aligned_count = Common::AlignDownPow2(count, 4); + u32 i = 4; + for (; i < aligned_count; i += 4) + { + const __m128 v(_mm_loadu_ps(&values[i])); + vmin = _mm_min_ps(v); + vmax = _mm_max_ps(v); + } + +#ifdef _MSC_VER + float min = std::min(vmin.m128_f32[0], std::min(vmin.m128_f32[1], std::min(vmin.m128_f32[2], vmin.m128_f32[3]))); + float max = std::max(vmax.m128_f32[0], std::max(vmax.m128_f32[1], std::max(vmax.m128_f32[2], vmax.m128_f32[3]))); +#else + float min = std::min(vmin[0], std::min(vmin[1], std::min(vmin[2], vmin[3]))); + float max = std::max(vmax[0], std::max(vmax[1], std::max(vmax[2], vmax[3]))); +#endif + for (; i < count; i++) + { + min = std::min(min, values[i]); + max = std::max(max, values[i]); + } + + return std::tie(min, max); +#elif defined(CPU_AARCH64) + float32x4_t vmin(vld1q_f32(values.data())); + float32x4_t vmax(vmin); + + const u32 count = static_cast(values.size()); + const u32 aligned_count = Common::AlignDownPow2(count, 4); + u32 i = 4; + for (; i < aligned_count; i += 4) + { + const float32x4_t v(vld1q_f32(&values[i])); + vmin = vminq_f32(v); + vmax = vmaxq_f32(v); + } + + float min = vminvq_f32(vmin); + float max = vmaxvq_f32(vmax); + for (; i < count; i++) + { + min = std::min(min, values[i]); + max = std::max(max, values[i]); + } + + return std::tie(min, max); +#else + float min = values[0]; + float max = values[0]; + const u32 count = static_cast(values.size()); + for (u32 i = 1; i < count; i++) + { + min = std::min(min, values[i]); + max = std::max(max, values[i]); + } + + return std::tie(min, max); +#endif +} + + static bool s_save_state_selector_ui_open = false; void ImGuiManager::RenderOverlays() @@ -160,7 +240,7 @@ void ImGuiManager::DrawPerformanceOverlay() if (g_settings.display_show_cpu) { text.Clear(); - text.AppendFmtString("{:.2f}ms ({:.2f}ms worst)", System::GetAverageFrameTime(), System::GetWorstFrameTime()); + text.AppendFmtString("{:.2f}ms | {:.2f}ms | {:.2f}ms", System::GetMinimumFrameTime(), System::GetMaximumFrameTime(), System::GetAverageFrameTime()); DRAW_LINE(fixed_font, text, IM_COL32(255, 255, 255, 255)); text.Clear(); @@ -241,6 +321,70 @@ void ImGuiManager::DrawPerformanceOverlay() DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255)); } } + + if (g_settings.display_show_frame_times) + { + const ImVec2 history_size(200.0f * scale, 50.0f * scale); + ImGui::SetNextWindowSize(ImVec2(history_size.x, history_size.y)); + ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x - margin - history_size.x, position_y)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.25f)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_PlotLines, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f); + ImGui::PushFont(fixed_font); + if (ImGui::Begin("##frame_times", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs)) + { + auto [min, max] = GetMinMax(System::GetFrameTimeHistory()); + + // add a little bit of space either side, so we're not constantly resizing + if ((max - min) < 4.0f) + { + min = min - std::fmod(min, 1.0f); + max = max - std::fmod(max, 1.0f) + 1.0f; + min = std::max(min - 2.0f, 0.0f); + max += 2.0f; + } + + ImGui::PlotEx( + ImGuiPlotType_Lines, "##frame_times", + [](void*, int idx) -> float { + return System::GetFrameTimeHistory()[((System::GetFrameTimeHistoryPos() + idx) % + System::NUM_FRAME_TIME_SAMPLES)]; + }, + nullptr, System::NUM_FRAME_TIME_SAMPLES, 0, nullptr, min, max, history_size); + + ImDrawList* win_dl = ImGui::GetCurrentWindow()->DrawList; + const ImVec2 wpos(ImGui::GetCurrentWindow()->Pos); + + text.Clear(); + text.AppendFmtString("{:.1f} ms", max); + text_size = fixed_font->CalcTextSizeA(fixed_font->FontSize, FLT_MAX, 0.0f, text.GetCharArray(), + text.GetCharArray() + text.GetLength()); + win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing + shadow_offset, wpos.y + shadow_offset), + IM_COL32(0, 0, 0, 100), text.GetCharArray(), text.GetCharArray() + text.GetLength()); + win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing, wpos.y), IM_COL32(255, 255, 255, 255), + text.GetCharArray(), text.GetCharArray() + text.GetLength()); + + text.Clear(); + text.AppendFmtString("{:.1f} ms", min); + text_size = fixed_font->CalcTextSizeA(fixed_font->FontSize, FLT_MAX, 0.0f, text.GetCharArray(), + text.GetCharArray() + text.GetLength()); + win_dl->AddText(ImVec2(wpos.x + history_size.x - text_size.x - spacing + shadow_offset, + wpos.y + history_size.y - fixed_font->FontSize + shadow_offset), + IM_COL32(0, 0, 0, 100), text.GetCharArray(), text.GetCharArray() + text.GetLength()); + win_dl->AddText( + ImVec2(wpos.x + history_size.x - text_size.x - spacing, wpos.y + history_size.y - fixed_font->FontSize), + IM_COL32(255, 255, 255, 255), text.GetCharArray(), text.GetCharArray() + text.GetLength()); + } + ImGui::End(); + ImGui::PopFont(); + ImGui::PopStyleVar(5); + ImGui::PopStyleColor(3); + } } else if (g_settings.display_show_status_indicators && state == System::State::Paused && !FullscreenUI::HasActiveWindow())